Repository: All-Hands-AI/OpenHands
Pull request: Fix Feedback Submission Retry
## Problem
Users often encounter failures when submitting feedback for the first time, requiring a manual second click to successfully submit. This issue doesn't occur 100% of the time but happens frequently enough to impact user experience.

## Solution
1. Added automatic retry mechanism that attempts submission up to 2 times when initial submission fails
2. Improved UI interaction by displaying submission status indicators
3. Added i18n support for submission status text

## Technical Implementation
- Enhanced `use-submit-feedback.ts` with retry logic including a 500ms delay between retries to avoid consecutive request failures
- Updated `feedback-form.tsx` to track and display submission status
- Added new internationalization keys to `i18n/declaration.ts`: `FEEDBACK$SUBMITTING_LABEL` and `FEEDBACK$SUBMITTING_MESSAGE`

## Modified Files
- frontend/src/hooks/mutation/use-submit-feedback.ts
- frontend/src/components/features/feedback/feedback-form.tsx
- frontend/src/i18n/declaration.ts

## Related Issue
Fixes #8637

## Testing
1. Verified normal feedback submission flow
2. Simulated network errors to confirm automatic retry functionality
3. Confirmed proper status text display across different language settings


Problem statement:
[Bug]: Submitting feedback sometimes does not work on first try
### Is there an existing issue for the same bug? (If one exists, thumbs up or comment on the issue instead).

- [x] I have checked the existing issues.

### Describe the bug and reproduction steps

* Use a simple prompt like "Create a hello world script in bash"
* Press thumbs up button
* Put in email and submit
* Get an error
* Click submit again
* Works

This is not 100% reproducible. But happens often enough.

### OpenHands Installation

app.all-hands.dev

### OpenHands Version

0.39.0

### Operating System

None

### Logs, Errors, Screenshots, and Additional Context

_No response_

Hints from issue discussion:

Commit message: fix: 修复提交反馈首次尝试失败问题 (#8637)

Patch to review:
diff --git a/README_CN.md b/README_CN.md
new file mode 100644
index 000000000000..df43b9c1739c
--- /dev/null
+++ b/README_CN.md
@@ -0,0 +1,146 @@
+
+<a name="readme-top"></a>
+
+<div align="center">
+  <img src="./docs/static/img/logo.png" alt="Logo" width="200">
+  <h1 align="center">OpenHands: 少写代码，多做事</h1>
+</div>
+
+
+<div align="center">
+  <a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
+  <a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
+  <a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
+  <br/>
+  <a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
+  <a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
+  <a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
+  <br/>
+  <a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
+  <a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
+  <a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
+  <hr>
+</div>
+
+欢迎使用OpenHands（前身为OpenDevin），这是一个由AI驱动的软件开发代理平台。
+
+OpenHands代理可以完成人类开发者能做的任何事情：修改代码、运行命令、浏览网页、调用API，甚至从StackOverflow复制代码片段。
+
+在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息，或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
+
+> [!IMPORTANT]
+> 在工作中使用OpenHands？我们很想与您交流！填写
+> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
+> 加入我们的设计合作伙伴计划，您将获得商业功能的早期访问权限，并有机会对我们的产品路线图提供意见。
+
+![应用截图](./docs/static/img/screenshot.png)
+
+## ☁️ OpenHands Cloud
+开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上，
+新用户可获得$50的免费额度。
+
+## 💻 在本地运行OpenHands
+
+OpenHands也可以使用Docker在本地系统上运行。
+查看[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)指南了解
+系统要求和更多信息。
+
+> [!WARNING]
+> 在公共网络上？请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
+> 通过限制网络绑定和实施其他安全措施来保护您的部署。
+
+
+```bash
+docker pull docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik
+
+docker run -it --rm --pull=always \
+    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik \
+    -e LOG_ALL_EVENTS=true \
+    -v /var/run/docker.sock:/var/run/docker.sock \
+    -v ~/.openhands-state:/.openhands-state \
+    -p 3000:3000 \
+    --add-host host.docker.internal:host-gateway \
+    --name openhands-app \
+    docker.all-hands.dev/all-hands-ai/openhands:0.39
+```
+
+您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands！
+
+打开应用程序时，您将被要求选择一个LLM提供商并添加API密钥。
+[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)（`anthropic/claude-sonnet-4-20250514`）
+效果最佳，但您还有[许多选择](https://docs.all-hands.dev/modules/usage/llms)。
+
+## 💡 运行OpenHands的其他方式
+
+> [!CAUTION]
+> OpenHands旨在由单个用户在其本地工作站上运行。
+> 它不适合多租户部署，即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
+>
+> 如果您有兴趣在多租户环境中运行OpenHands，请
+> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
+> 了解高级部署选项。
+
+您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem)，
+以可编程的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)运行OpenHands，
+通过[友好的CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode)与其交互，
+或使用[GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)在标记的问题上运行它。
+
+访问[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)获取更多信息和设置说明。
+
+如果您想修改OpenHands源代码，请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
+
+遇到问题？[故障排除指南](https://docs.all-hands.dev/modules/usage/troubleshooting)可以提供帮助。
+
+## 📖 文档
+  <a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
+
+要了解有关项目的更多信息，以及使用OpenHands的技巧，
+请查看我们的[文档](https://docs.all-hands.dev/modules/usage/getting-started)。
+
+在那里，您将找到有关如何使用不同LLM提供商、
+故障排除资源和高级配置选项的资源。
+
+## 🤝 如何加入社区
+
+OpenHands是一个社区驱动的项目，我们欢迎每个人的贡献。我们大部分沟通
+通过Slack进行，因此这是开始的最佳场所，但我们也很乐意您通过Discord或Github与我们联系：
+
+- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
+- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器，用于一般讨论、问题和反馈。
+- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题，或添加您自己的想法。
+
+在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息，或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
+
+## 📈 进展
+
+在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图（每月月底在维护者会议上更新）。
+
+<p align="center">
+  <a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
+    <img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
+  </a>
+</p>
+
+## 📜 许可证
+
+根据MIT许可证分发。有关更多信息，请参阅[`LICENSE`](./LICENSE)。
+
+## 🙏 致谢
+
+OpenHands由大量贡献者构建，每一份贡献都备受感谢！我们还借鉴了其他开源项目，对他们的工作深表感谢。
+
+有关OpenHands中使用的开源项目和许可证列表，请参阅我们的[CREDITS.md](./CREDITS.md)文件。
+
+## 📚 引用
+
+```
+@misc{openhands,
+      title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
+      author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
+      year={2024},
+      eprint={2407.16741},
+      archivePrefix={arXiv},
+      primaryClass={cs.SE},
+      url={https://arxiv.org/abs/2407.16741},
+}
+```
diff --git a/frontend/src/components/features/feedback/feedback-form.tsx b/frontend/src/components/features/feedback/feedback-form.tsx
index fcf1e666ddc4..eb60c282cf0d 100644
--- a/frontend/src/components/features/feedback/feedback-form.tsx
+++ b/frontend/src/components/features/feedback/feedback-form.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
 import hotToast from "react-hot-toast";
 import { useTranslation } from "react-i18next";
 import { I18nKey } from "#/i18n/declaration";
@@ -16,6 +16,8 @@ interface FeedbackFormProps {
 
 export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
   const { t } = useTranslation();
+  const [submitAttempted, setSubmitAttempted] = useState(false);
+
   const copiedToClipboardToast = () => {
     hotToast(t(I18nKey.FEEDBACK$PASSWORD_COPIED_MESSAGE), {
       icon: "📋",
@@ -61,6 +63,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
 
   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     event?.preventDefault();
+    setSubmitAttempted(true);
     const formData = new FormData(event.currentTarget);
 
     const email = formData.get("email")?.toString() || "";
@@ -83,8 +86,12 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
           const { message, feedback_id, password } = data.body; // eslint-disable-line
           const link = `${VIEWER_PAGE}?share_id=${feedback_id}`;
           shareFeedbackToast(message, link, password);
+          setSubmitAttempted(false);
           onClose();
         },
+        onError: () => {
+          setSubmitAttempted(false);
+        }
       },
     );
   };
@@ -127,7 +134,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
           className="grow"
           isDisabled={isPending}
         >
-          {t(I18nKey.FEEDBACK$SHARE_LABEL)}
+          {isPending ? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) : t(I18nKey.FEEDBACK$SHARE_LABEL)}
         </BrandButton>
         <BrandButton
           type="button"
@@ -139,6 +146,11 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
           {t(I18nKey.FEEDBACK$CANCEL_LABEL)}
         </BrandButton>
       </div>
+      {submitAttempted && isPending && (
+        <p className="text-sm text-center text-neutral-400">
+          {t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE)}
+        </p>
+      )}
     </form>
   );
 }
diff --git a/frontend/src/hooks/mutation/use-submit-feedback.ts b/frontend/src/hooks/mutation/use-submit-feedback.ts
index 8f8355f51804..69d072c862e4 100644
--- a/frontend/src/hooks/mutation/use-submit-feedback.ts
+++ b/frontend/src/hooks/mutation/use-submit-feedback.ts
@@ -11,8 +11,28 @@ type SubmitFeedbackArgs = {
 export const useSubmitFeedback = () => {
   const { conversationId } = useConversationId();
   return useMutation({
-    mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
-      OpenHands.submitFeedback(conversationId, feedback),
+    mutationFn: async ({ feedback }: SubmitFeedbackArgs) => {
+      // 添加重试逻辑
+      const maxRetries = 2;
+      let retryCount = 0;
+      let lastError;
+
+      while (retryCount < maxRetries) {
+        try {
+          return await OpenHands.submitFeedback(conversationId, feedback);
+        } catch (error) {
+          lastError = error;
+          retryCount++;
+          // 如果不是最后一次尝试，不显示错误提示
+          if (retryCount < maxRetries) {
+            // 等待短暂时间后重试
+            await new Promise(resolve => setTimeout(resolve, 500));
+          }
+        }
+      }
+      // 所有重试都失败后抛出最后的错误
+      throw lastError;
+    },
     onError: (error) => {
       displayErrorToast(error.message);
     },
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts
index fec19d7480a1..bf69516d96a3 100644
--- a/frontend/src/i18n/declaration.ts
+++ b/frontend/src/i18n/declaration.ts
@@ -228,6 +228,8 @@ export enum I18nKey {
   FEEDBACK$FAILED_TO_SHARE = "FEEDBACK$FAILED_TO_SHARE",
   FEEDBACK$COPY_LABEL = "FEEDBACK$COPY_LABEL",
   FEEDBACK$SHARING_SETTINGS_LABEL = "FEEDBACK$SHARING_SETTINGS_LABEL",
+  FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
+  FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
   SECURITY$UNKNOWN_ANALYZER_LABEL = "SECURITY$UNKNOWN_ANALYZER_LABEL",
   INVARIANT$UPDATE_POLICY_LABEL = "INVARIANT$UPDATE_POLICY_LABEL",
   INVARIANT$UPDATE_SETTINGS_LABEL = "INVARIANT$UPDATE_SETTINGS_LABEL",
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index 66f4bb0f6e60..4af617a8ef27 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -8766,5 +8766,37 @@
         "tr": "Uzman ipucu",
         "de": "Profi-Tipp",
         "uk": "Порада професіонала"
+    },
+    "FEEDBACK$SUBMITTING_LABEL": {
+        "en": "Submitting...",
+        "ja": "送信中...",
+        "zh-CN": "提交中...",
+        "zh-TW": "提交中...",
+        "ko-KR": "제출 중...",
+        "no": "Sender...",
+        "it": "Inviando...",
+        "pt": "Enviando...",
+        "es": "Enviando...",
+        "ar": "إرسال...",
+        "fr": "Envoi...",
+        "tr": "Gönderiliyor...",
+        "de": "Senden...",  
+        "uk": "Відправляємо..."
+    },
+    "FEEDBACK$SUBMITTING_MESSAGE": {
+        "en": "Submitting feedback, please wait...",
+        "ja": "フィードバックを送信中です。しばらくお待ちください...",
+        "zh-CN": "正在提交反馈，请稍候...",
+        "zh-TW": "正在提交回饋，請稍候...",
+        "ko-KR": "피드백을 제출하고 있습니다. 잠시만 기다려주세요...",
+        "no": "Sender inn feedback, vennligst vent...",
+        "it": "Invio feedback, attendi...",
+        "pt": "Enviando feedback, por favor aguarde...",
+        "es": "Enviando comentarios, por favor espere...",
+        "ar": "إرسال التغذية الرجعية، يرجى الإنتظار...",
+        "fr": "Envoi de commentaires, veuillez patienter...",
+        "tr": "Geri bildirim gönderiliyor, lütfen bekleyin...",
+        "de": "Feedback senden, bitte warten...",
+        "uk": "Відправляємо відгук, будь ласка, почекайте..."
     }
 }


Code review comments:
path: frontend/src/hooks/mutation/use-submit-feedback.ts
text: @user1:
TanStack mutation hooks have a `retry` and `retryDelay` option that should be used instead of implementing custom retry logic.

See [here](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation)

@author:
@user1 Thank you for your review and suggestions! I have made the following changes as requested:

1. Removed the `submitAttempted` state and now rely solely on `isPending` for handling submission status display
2. Used TanStack's built-in `retry: 2` and `retryDelay: 500` options instead of custom retry logic
3. Fixed code formatting issues

All changes have been implemented and lint errors fixed. Thank you for your guidance which has made the code more concise and aligned with best practices!

diff_hunk: @@ -139,6 +146,11 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
           {t(I18nKey.FEEDBACK$CANCEL_LABEL)}
         </BrandButton>
       </div>
+      {submitAttempted && isPending && (
path: frontend/src/components/features/feedback/feedback-form.tsx
text: @user1:
I think we can remove `submitAttempted` and rely on `isPending` only instead