Thinking stream (#2276)

* feat: stream thinking tokens

* fix: retry bug

* fix test
This commit is contained in:
Alex
2026-02-09 14:27:53 +00:00
committed by GitHub
parent fea94379d7
commit 36c7bd9206
8 changed files with 103 additions and 36 deletions

View File

@@ -195,12 +195,21 @@ export const agentPreviewSlice = createSlice({
},
resendQuery(
state,
action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
action: PayloadAction<{ index: number; prompt: string }>,
) {
state.queries = [
...state.queries.splice(0, action.payload.index),
action.payload,
];
const { index, prompt } = action.payload;
if (index < 0 || index >= state.queries.length) return;
state.queries.splice(index + 1);
state.queries[index].prompt = prompt;
delete state.queries[index].response;
delete state.queries[index].thought;
delete state.queries[index].sources;
delete state.queries[index].tool_calls;
delete state.queries[index].error;
delete state.queries[index].structured;
delete state.queries[index].schema;
delete state.queries[index].feedback;
},
updateStreamingQuery(
state,
@@ -309,10 +318,13 @@ export const agentPreviewSlice = createSlice({
.addCase(fetchPreviewAnswer.rejected, (state, action) => {
if (action.meta.aborted) {
state.status = 'idle';
return state;
return;
}
state.status = 'failed';
state.queries[state.queries.length - 1].error = 'Something went wrong';
if (state.queries.length > 0) {
state.queries[state.queries.length - 1].error =
'Something went wrong';
}
});
},
});

View File

@@ -120,18 +120,20 @@ export default function Conversation() {
if (updated === true) {
handleQuestion({ question: question as string, index: indx });
} else if (question && status !== 'loading') {
if (lastQueryReturnedErr) {
if (lastQueryReturnedErr && queries.length > 0) {
const retryIndex = queries.length - 1;
dispatch(
updateQuery({
index: queries.length - 1,
index: retryIndex,
query: {
prompt: question,
},
}),
);
handleQuestion({
question: question,
question,
isRetry: true,
index: retryIndex,
});
} else {
handleQuestion({
@@ -152,11 +154,14 @@ export default function Conversation() {
};
useEffect(() => {
if (queries.length) {
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
queries[queries.length - 1].response && setLastQueryReturnedErr(false);
if (queries.length === 0) {
setLastQueryReturnedErr(false);
return;
}
}, [queries[queries.length - 1]]);
const lastQuery = queries[queries.length - 1];
setLastQueryReturnedErr(!!lastQuery.error && !lastQuery.response);
}, [queries]);
return (
<div className="flex h-full flex-col justify-end gap-1">

View File

@@ -24,13 +24,12 @@ type ConversationMessagesProps = {
handleQuestion: (params: {
question: string;
isRetry?: boolean;
updated?: boolean | null;
indx?: number;
index?: number;
}) => void;
handleQuestionSubmission: (
updatedQuestion?: string,
updated?: boolean,
indx?: number,
index?: number,
) => void;
handleFeedback?: (query: Query, feedback: FEEDBACK, index: number) => void;
queries: Query[];
@@ -169,7 +168,7 @@ export default function ConversationMessages({
handleQuestion({
question: questionToRetry,
isRetry: true,
indx: index,
index,
});
}}
aria-label={t('Retry') || 'Retry'}

View File

@@ -241,12 +241,21 @@ export const conversationSlice = createSlice({
},
resendQuery(
state,
action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
action: PayloadAction<{ index: number; prompt: string }>,
) {
state.queries = [
...state.queries.splice(0, action.payload.index),
action.payload,
];
const { index, prompt } = action.payload;
if (index < 0 || index >= state.queries.length) return;
state.queries.splice(index + 1);
state.queries[index].prompt = prompt;
delete state.queries[index].response;
delete state.queries[index].thought;
delete state.queries[index].sources;
delete state.queries[index].tool_calls;
delete state.queries[index].error;
delete state.queries[index].structured;
delete state.queries[index].schema;
delete state.queries[index].feedback;
},
updateStreamingQuery(
state,
@@ -370,7 +379,7 @@ export const conversationSlice = createSlice({
.addCase(fetchAnswer.rejected, (state, action) => {
if (action.meta.aborted) {
state.status = 'idle';
return state;
return;
}
state.status = 'failed';
if (state.queries.length > 0) {

View File

@@ -266,10 +266,13 @@ export const sharedConversationSlice = createSlice({
.addCase(fetchSharedAnswer.rejected, (state, action) => {
if (action.meta.aborted) {
state.status = 'idle';
return state;
return;
}
state.status = 'failed';
state.queries[state.queries.length - 1].error = 'Something went wrong';
if (state.queries.length > 0) {
state.queries[state.queries.length - 1].error =
'Something went wrong';
}
});
},
});

View File

@@ -90,12 +90,49 @@ export function getLocalApiKey(): string | null {
return key;
}
export function getLocalRecentDocs(sourceDocs?: Doc[] | null): Doc[] | null {
const docsString = localStorage.getItem('DocsGPTRecentDocs');
const selectedDocs = docsString ? (JSON.parse(docsString) as Doc[]) : null;
function parseStoredRecentDocs(docsString: string | null): Doc[] | null {
if (!docsString) {
return null;
}
if (!sourceDocs || !selectedDocs || selectedDocs.length === 0) {
return selectedDocs;
try {
const parsedDocs: unknown = JSON.parse(docsString);
if (Array.isArray(parsedDocs)) {
const docs = parsedDocs.filter(
(doc): doc is Doc => typeof doc === 'object' && doc !== null,
);
return docs.length > 0 ? docs : null;
}
if (typeof parsedDocs === 'object' && parsedDocs !== null) {
return [parsedDocs as Doc];
}
} catch (error) {
console.warn('Failed to parse DocsGPTRecentDocs from localStorage', error);
}
return null;
}
export function getStoredRecentDocs(): Doc[] {
const recentDocs = parseStoredRecentDocs(
localStorage.getItem('DocsGPTRecentDocs'),
);
if (!recentDocs || recentDocs.length === 0) {
localStorage.removeItem('DocsGPTRecentDocs');
return [];
}
return recentDocs;
}
export function getLocalRecentDocs(sourceDocs?: Doc[] | null): Doc[] | null {
const selectedDocs = getStoredRecentDocs();
if (!sourceDocs || selectedDocs.length === 0) {
return selectedDocs.length > 0 ? selectedDocs : null;
}
const isDocAvailable = (selected: Doc) => {
return sourceDocs.some((source) => {

View File

@@ -3,6 +3,7 @@ import { configureStore } from '@reduxjs/toolkit';
import agentPreviewReducer from './agents/agentPreviewSlice';
import { conversationSlice } from './conversation/conversationSlice';
import { sharedConversationSlice } from './conversation/sharedConversationSlice';
import { getStoredRecentDocs } from './preferences/preferenceApi';
import {
Preference,
prefListenerMiddleware,
@@ -13,7 +14,6 @@ import uploadReducer from './upload/uploadSlice';
const key = localStorage.getItem('DocsGPTApiKey');
const prompt = localStorage.getItem('DocsGPTPrompt');
const chunks = localStorage.getItem('DocsGPTChunks');
const doc = localStorage.getItem('DocsGPTRecentDocs');
const selectedModel = localStorage.getItem('DocsGPTSelectedModel');
const preloadedState: { preference: Preference } = {
@@ -30,7 +30,7 @@ const preloadedState: { preference: Preference } = {
{ name: 'strict', id: 'strict', type: 'public' },
],
chunks: JSON.parse(chunks ?? '2').toString(),
selectedDocs: doc !== null ? JSON.parse(doc) : [],
selectedDocs: getStoredRecentDocs(),
conversations: {
data: null,
loading: false,

View File

@@ -1,4 +1,4 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '../store';
export interface Attachment {
@@ -147,8 +147,10 @@ export const {
} = uploadSlice.actions;
export const selectAttachments = (state: RootState) => state.upload.attachments;
export const selectCompletedAttachments = (state: RootState) =>
state.upload.attachments.filter((att) => att.status === 'completed');
export const selectCompletedAttachments = createSelector(
[selectAttachments],
(attachments) => attachments.filter((att) => att.status === 'completed'),
);
export const selectUploadTasks = (state: RootState) => state.upload.tasks;
export default uploadSlice.reducer;