-import { Bot } from "@lucide/vue";
+import { Bot, RotateCcw } from "@lucide/vue";
import MarkdownIt from "markdown-it";
import { computed } from "vue";
@@ -11,6 +11,12 @@ const props = defineProps<{
showReasoning?: boolean;
createdAt: string;
streaming?: boolean;
+ errorMessage?: string;
+ canRetry?: boolean;
+}>();
+
+const emit = defineEmits<{
+ retry: [];
}>();
const markdown = new MarkdownIt({ breaks: true, html: false, linkify: true });
@@ -95,6 +101,13 @@ const displayTime = computed(() => {
思考中
+
+ {{ errorMessage }}
+
+
{{ renderedContent }}
diff --git a/ai_knowledge_base_v2/apps/user-client/src/components/MessageList.vue b/ai_knowledge_base_v2/apps/user-client/src/components/MessageList.vue
index 70d08b0..fef0a25 100644
--- a/ai_knowledge_base_v2/apps/user-client/src/components/MessageList.vue
+++ b/ai_knowledge_base_v2/apps/user-client/src/components/MessageList.vue
@@ -11,6 +11,8 @@ export interface DisplayMessage {
showReasoning?: boolean;
createdAt: string;
streaming?: boolean;
+ errorMessage?: string;
+ retryQuestion?: string;
}
defineProps<{
@@ -20,6 +22,7 @@ defineProps<{
const emit = defineEmits<{
followChange: [following: boolean];
+ retry: [messageId: string];
}>();
const scroller = ref(null);
@@ -63,6 +66,9 @@ defineExpose({ scrollToBottom, scrollToMessage });
:show-reasoning="message.showReasoning"
:created-at="message.createdAt"
:streaming="message.streaming"
+ :error-message="message.errorMessage"
+ :can-retry="Boolean(message.retryQuestion)"
+ @retry="emit('retry', message.id)"
/>
diff --git a/ai_knowledge_base_v2/apps/user-client/src/services/api.ts b/ai_knowledge_base_v2/apps/user-client/src/services/api.ts
index d0bc41b..1e75fbb 100644
--- a/ai_knowledge_base_v2/apps/user-client/src/services/api.ts
+++ b/ai_knowledge_base_v2/apps/user-client/src/services/api.ts
@@ -123,6 +123,7 @@ export async function streamChat(
onReasoning?: (chunk: string) => void,
onStatus?: (message: string, type: string, reasoningVisible: boolean) => void,
signal?: AbortSignal,
+ retry = false,
) {
const token = getToken();
const response = await fetch(`${API_BASE}/chat/completions`, {
@@ -131,7 +132,7 @@ export async function streamChat(
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
- body: JSON.stringify({ sessionId, message }),
+ body: JSON.stringify({ sessionId, message, retry }),
signal,
});
if (!response.ok || !response.body) {
diff --git a/ai_knowledge_base_v2/apps/user-client/src/styles.css b/ai_knowledge_base_v2/apps/user-client/src/styles.css
index 76639a8..0641c85 100644
--- a/ai_knowledge_base_v2/apps/user-client/src/styles.css
+++ b/ai_knowledge_base_v2/apps/user-client/src/styles.css
@@ -1400,6 +1400,51 @@ textarea:focus-visible {
.generation-state span:nth-child(2) { animation-delay: 0.14s; }
.generation-state span:nth-child(3) { animation-delay: 0.28s; margin-right: 4px; }
+.message-error-state {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 9px 12px;
+ margin-top: 10px;
+ padding: 10px 11px;
+ border: 1px solid #ead5cf;
+ border-radius: 11px;
+ background: #fff8f5;
+ color: #8a5043;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.message-error-state span {
+ flex: 1 1 190px;
+}
+
+.message-error-state button {
+ min-height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 0 11px;
+ border: 1px solid #bf7765;
+ border-radius: 9px;
+ background: #ffffff;
+ color: #8a4434;
+ font: inherit;
+ font-weight: 700;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.message-error-state button:hover {
+ background: #fff0eb;
+}
+
+.message-error-state button:disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
@keyframes generation-pulse {
0%, 70%, 100% { opacity: 0.25; transform: translateY(0); }
35% { opacity: 1; transform: translateY(-2px); }