Update App.tsx
This commit is contained in:
+64
-16
@@ -102,6 +102,10 @@ export function App() {
|
|||||||
const [filePreview, setFilePreview] = useState<FilePreview | null>(null);
|
const [filePreview, setFilePreview] = useState<FilePreview | null>(null);
|
||||||
const [previewBusy, setPreviewBusy] = useState(false);
|
const [previewBusy, setPreviewBusy] = useState(false);
|
||||||
const [ocrAvailable, setOcrAvailable] = useState(false);
|
const [ocrAvailable, setOcrAvailable] = useState(false);
|
||||||
|
// Model-error retry: which trailing message is the error bubble (if any),
|
||||||
|
// its conversation, and how many times this prompt was already retried.
|
||||||
|
const [failed, setFailed] = useState<{ index: number; convId: string } | null>(null);
|
||||||
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
const { t } = useT();
|
const { t } = useT();
|
||||||
|
|
||||||
// Public routes: no auth needed. IDs are constant for the page lifetime.
|
// Public routes: no auth needed. IDs are constant for the page lifetime.
|
||||||
@@ -199,6 +203,8 @@ export function App() {
|
|||||||
|
|
||||||
// Load messages for active conversation
|
// Load messages for active conversation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setFailed(null);
|
||||||
|
setRetryCount(0);
|
||||||
if (!activeId) {
|
if (!activeId) {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
return;
|
return;
|
||||||
@@ -248,6 +254,8 @@ export function App() {
|
|||||||
setChecking(false);
|
setChecking(false);
|
||||||
setConvs([]);
|
setConvs([]);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
|
setFailed(null);
|
||||||
|
setRetryCount(0);
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
setArchivedConv(null);
|
setArchivedConv(null);
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
@@ -265,6 +273,27 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Max footer-arrow retries for the same failed prompt. */
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
|
||||||
|
/** Stream one assistant reply for an exact history; resolves its text. */
|
||||||
|
async function streamReply(history: ChatMessage[], convId: string): Promise<string> {
|
||||||
|
// "Thinking" feature flag: decorate the outgoing payload only — the
|
||||||
|
// system message is never shown nor persisted client-side.
|
||||||
|
const outMessages = featureEnabled("thinking")
|
||||||
|
? [
|
||||||
|
{ role: "system", content: THINKING_SYSTEM_PROMPT } as ChatMessage,
|
||||||
|
...history,
|
||||||
|
]
|
||||||
|
: history;
|
||||||
|
let full = "";
|
||||||
|
await api.chatStream({ model, messages: outMessages, conversationId: convId }, (t) => {
|
||||||
|
full += t;
|
||||||
|
setStreaming(full);
|
||||||
|
});
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
|
||||||
async function send(text: string) {
|
async function send(text: string) {
|
||||||
if (sending || !model || archivedConv) return;
|
if (sending || !model || archivedConv) return;
|
||||||
let convId = activeId;
|
let convId = activeId;
|
||||||
@@ -291,25 +320,12 @@ export function App() {
|
|||||||
const history = [...messages, userMsg];
|
const history = [...messages, userMsg];
|
||||||
setMessages(history);
|
setMessages(history);
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
|
setFailed(null);
|
||||||
|
setRetryCount(0);
|
||||||
setSending(true);
|
setSending(true);
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
let full = "";
|
|
||||||
try {
|
try {
|
||||||
// "Thinking" feature flag: decorate the outgoing payload only — the
|
const full = await streamReply(history, convId!);
|
||||||
// system message is never shown nor persisted client-side.
|
|
||||||
const outMessages = featureEnabled("thinking")
|
|
||||||
? [
|
|
||||||
{ role: "system", content: THINKING_SYSTEM_PROMPT } as ChatMessage,
|
|
||||||
...history,
|
|
||||||
]
|
|
||||||
: history;
|
|
||||||
await api.chatStream(
|
|
||||||
{ model, messages: outMessages, conversationId: convId! },
|
|
||||||
(t) => {
|
|
||||||
full += t;
|
|
||||||
setStreaming(full);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setMessages([...history, { role: "assistant", content: full }]);
|
setMessages([...history, { role: "assistant", content: full }]);
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
await refreshConvs();
|
await refreshConvs();
|
||||||
@@ -318,6 +334,38 @@ export function App() {
|
|||||||
...history,
|
...history,
|
||||||
{ role: "assistant", content: t("attach.chatError") },
|
{ role: "assistant", content: t("attach.chatError") },
|
||||||
]);
|
]);
|
||||||
|
setFailed({ index: history.length, convId: convId! });
|
||||||
|
setStreaming("");
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-run the same prompt after a model error (footer retry arrow on the
|
||||||
|
* error bubble). The trailing error message is dropped and the untouched
|
||||||
|
* history is streamed again — at most MAX_RETRIES times per prompt.
|
||||||
|
*/
|
||||||
|
async function retryFailed() {
|
||||||
|
if (sending || !failed || retryCount >= MAX_RETRIES || !model || archivedConv) return;
|
||||||
|
const history = messages.slice(0, failed.index);
|
||||||
|
const convId = failed.convId;
|
||||||
|
setMessages(history);
|
||||||
|
setFailed(null);
|
||||||
|
setRetryCount((c) => c + 1);
|
||||||
|
setSending(true);
|
||||||
|
setStreaming("");
|
||||||
|
try {
|
||||||
|
const full = await streamReply(history, convId);
|
||||||
|
setMessages([...history, { role: "assistant", content: full }]);
|
||||||
|
setRetryCount(0);
|
||||||
|
await refreshConvs();
|
||||||
|
} catch {
|
||||||
|
setMessages([
|
||||||
|
...history,
|
||||||
|
{ role: "assistant", content: t("attach.chatError") },
|
||||||
|
]);
|
||||||
|
setFailed({ index: history.length, convId });
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user