preview auto-refresh after chat tool calls, gmail sync label scoping, queue notify option, workspace-scoped chat sessions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,7 @@ export const EmailList = () => {
|
|||||||
|
|
||||||
const handleSync = async () => {
|
const handleSync = async () => {
|
||||||
try {
|
try {
|
||||||
await createJob({ lane: 'google-api', type: 'gmail-sync' });
|
await createJob({ lane: 'google-api', type: 'gmail-sync', notify: false });
|
||||||
toast.success('Gmail sync started');
|
toast.success('Gmail sync started');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to start sync');
|
toast.error('Failed to start sync');
|
||||||
|
|||||||
@@ -29,9 +29,14 @@ queueRouter.get('/jobs/:id', async (ctx) => {
|
|||||||
queueRouter.post('/jobs', async (ctx) => {
|
queueRouter.post('/jobs', async (ctx) => {
|
||||||
const user = ctx.get('user');
|
const user = ctx.get('user');
|
||||||
const body = ctx.get('body');
|
const body = ctx.get('body');
|
||||||
const { lane, type, meta } = body as { lane: string; type: string; meta?: Record<string, unknown> };
|
const { lane, type, meta, notify } = body as {
|
||||||
|
lane: string;
|
||||||
|
type: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
notify?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const job = await enqueue({ lane, type, userId: user.email, meta });
|
const job = await enqueue({ lane, type, userId: user.email, meta, notify });
|
||||||
return ctx.json(job, 201);
|
return ctx.json(job, 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export async function enqueue(params: EnqueueParams): Promise<Job> {
|
|||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
meta: params.meta,
|
meta: params.meta,
|
||||||
|
notify: params.notify,
|
||||||
};
|
};
|
||||||
|
|
||||||
await writeJob(job);
|
await writeJob(job);
|
||||||
@@ -175,7 +176,7 @@ async function runJob(job: Job) {
|
|||||||
fresh.completedAt = Date.now();
|
fresh.completedAt = Date.now();
|
||||||
await writeJob(fresh);
|
await writeJob(fresh);
|
||||||
console.error(`[queue] Job ${fresh.id} failed at step "${step.name}":`, errorMessage);
|
console.error(`[queue] Job ${fresh.id} failed at step "${step.name}":`, errorMessage);
|
||||||
await notifyFailure(fresh);
|
if (fresh.notify !== false) await notifyFailure(fresh);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +187,7 @@ async function runJob(job: Job) {
|
|||||||
final.completedAt = Date.now();
|
final.completedAt = Date.now();
|
||||||
await writeJob(final);
|
await writeJob(final);
|
||||||
console.log(`[queue] Job ${final.id} completed`);
|
console.log(`[queue] Job ${final.id} completed`);
|
||||||
await notifyCompletion(final);
|
if (final.notify !== false) await notifyCompletion(final);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,8 @@ const gmailSyncHandler: JobHandler = {
|
|||||||
|
|
||||||
if (year) {
|
if (year) {
|
||||||
// Year-scoped sync: count total emails first, then sync month by month
|
// Year-scoped sync: count total emails first, then sync month by month
|
||||||
const yearQuery = `after:${year}/1/1 before:${year + 1}/1/1`;
|
const labelScope = '(in:inbox OR in:sent OR in:trash OR in:spam)';
|
||||||
|
const yearQuery = `${labelScope} after:${year}/1/1 before:${year + 1}/1/1`;
|
||||||
await ctx.updateProgress({ current: 0, total: 0, label: 'Counting emails...' });
|
await ctx.updateProgress({ current: 0, total: 0, label: 'Counting emails...' });
|
||||||
const totalEmails = await countMessages(token, yearQuery);
|
const totalEmails = await countMessages(token, yearQuery);
|
||||||
console.log(`[gmail-sync] Pre-flight: ${totalEmails} emails for ${year}`);
|
console.log(`[gmail-sync] Pre-flight: ${totalEmails} emails for ${year}`);
|
||||||
@@ -392,7 +393,7 @@ const gmailSyncHandler: JobHandler = {
|
|||||||
for (let i = 0; i < months.length; i++) {
|
for (let i = 0; i < months.length; i++) {
|
||||||
const month = months[i]!;
|
const month = months[i]!;
|
||||||
await ctx.updateProgress({ current: totalSaved + totalSkipped, total: totalEmails, label: month.label });
|
await ctx.updateProgress({ current: totalSaved + totalSkipped, total: totalEmails, label: month.label });
|
||||||
const query = `after:${month.after} before:${month.before}`;
|
const query = `${labelScope} after:${month.after} before:${month.before}`;
|
||||||
const result = await syncInbox(token, db, ctx.job.userId, query, (p) => {
|
const result = await syncInbox(token, db, ctx.job.userId, query, (p) => {
|
||||||
const current = totalSaved + p.saved + p.skipped + p.errors;
|
const current = totalSaved + p.saved + p.skipped + p.errors;
|
||||||
const label = `${month.label} — Saved ${(totalSaved + p.saved).toLocaleString()} of ${totalEmails.toLocaleString()}`;
|
const label = `${month.label} — Saved ${(totalSaved + p.saved).toLocaleString()} of ${totalEmails.toLocaleString()}`;
|
||||||
@@ -410,12 +411,14 @@ const gmailSyncHandler: JobHandler = {
|
|||||||
}
|
}
|
||||||
await ctx.updateProgress({ current: totalEmails, total: totalEmails, label: 'Done' });
|
await ctx.updateProgress({ current: totalEmails, total: totalEmails, label: 'Done' });
|
||||||
} else {
|
} else {
|
||||||
// If we have a last_sync_date (e.g. from migration), scope the sync to only newer emails
|
// Scope to inbox + sent to avoid syncing trash/spam/drafts
|
||||||
const lastSyncDate = getSyncMeta(db, 'last_sync_date');
|
const lastSyncDate = getSyncMeta(db, 'last_sync_date');
|
||||||
let syncQuery: string | undefined;
|
const labelScope = 'in:inbox OR in:sent OR in:trash OR in:spam';
|
||||||
|
let syncQuery: string = labelScope;
|
||||||
if (lastSyncDate) {
|
if (lastSyncDate) {
|
||||||
const d = new Date(lastSyncDate);
|
const d = new Date(lastSyncDate);
|
||||||
syncQuery = `after:${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
|
const dateScope = `after:${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
|
||||||
|
syncQuery = `(${labelScope}) ${dateScope}`;
|
||||||
console.log(`[gmail-sync] Scoping full sync with query: ${syncQuery}`);
|
console.log(`[gmail-sync] Scoping full sync with query: ${syncQuery}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type Job = {
|
|||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
completedAt?: number;
|
completedAt?: number;
|
||||||
meta?: Record<string, unknown>;
|
meta?: Record<string, unknown>;
|
||||||
|
notify?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StepContext = {
|
export type StepContext = {
|
||||||
@@ -54,4 +55,5 @@ export type EnqueueParams = {
|
|||||||
type: string;
|
type: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
meta?: Record<string, unknown>;
|
meta?: Record<string, unknown>;
|
||||||
|
notify?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const useJobs = (filters?: JobFilters) => {
|
|||||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['jobs'] });
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['jobs'] });
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (params: { lane: string; type: string; meta?: Record<string, unknown> }) =>
|
mutationFn: (params: { lane: string; type: string; meta?: Record<string, unknown>; notify?: boolean }) =>
|
||||||
client.post<Job>('/queue/jobs', params),
|
client.post<Job>('/queue/jobs', params),
|
||||||
onSuccess: invalidate,
|
onSuccess: invalidate,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getProviderDisplayName } from 'state/useModels';
|
|||||||
type ChatSessionSelection = {
|
type ChatSessionSelection = {
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
|
workspaceId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatModel(model: string): string {
|
function formatModel(model: string): string {
|
||||||
@@ -35,18 +36,27 @@ export const ChatHeader = () => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const autoResumedRef = useRef(false);
|
const autoResumedRef = useRef(false);
|
||||||
|
|
||||||
// Auto-resume the latest session on mount
|
// Clear stale selection from a different workspace
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoResumedRef.current || selection) return;
|
if (selection && selection.workspaceId !== workspaceId) {
|
||||||
|
setSelection(null);
|
||||||
|
autoResumedRef.current = false;
|
||||||
|
}
|
||||||
|
}, [workspaceId, selection]);
|
||||||
|
|
||||||
|
// Auto-resume the latest session for this workspace
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoResumedRef.current) return;
|
||||||
|
if (selection && selection.workspaceId === workspaceId) return;
|
||||||
if (sessions.length > 0) {
|
if (sessions.length > 0) {
|
||||||
const latest = sessions[0]!;
|
const latest = sessions[0]!;
|
||||||
setSelection({ sessionId: latest.id, model: latest.model ?? null });
|
setSelection({ sessionId: latest.id, model: latest.model ?? null, workspaceId });
|
||||||
autoResumedRef.current = true;
|
autoResumedRef.current = true;
|
||||||
}
|
}
|
||||||
}, [sessions, selection]);
|
}, [sessions, selection, workspaceId]);
|
||||||
|
|
||||||
const selectSession = (sessionId: string | null, model?: string | null) => {
|
const selectSession = (sessionId: string | null, model?: string | null) => {
|
||||||
setSelection({ sessionId, model });
|
setSelection({ sessionId, model, workspaceId });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useWorkspace } from '../../components/Workspace';
|
import { useWorkspace } from '../../components/Workspace';
|
||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||||
import { usePiChat } from '../../hooks/usePiChat';
|
import { usePiChat } from '../../hooks/usePiChat';
|
||||||
@@ -7,6 +7,7 @@ import { EmbeddableChat } from './EmbeddableChat';
|
|||||||
type ChatSessionSelection = {
|
type ChatSessionSelection = {
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
|
workspaceId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChatPanelInnerProps = {
|
type ChatPanelInnerProps = {
|
||||||
@@ -18,10 +19,11 @@ type ChatPanelInnerProps = {
|
|||||||
promptPrefix?: string;
|
promptPrefix?: string;
|
||||||
chatContext: Record<string, string | undefined>;
|
chatContext: Record<string, string | undefined>;
|
||||||
setActiveSession: (id: string | null) => void;
|
setActiveSession: (id: string | null) => void;
|
||||||
|
onTurnComplete?: (hadToolCalls: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatPanelInner = ({ sessionId, model, scoped, sandboxed, cwdParam, promptPrefix, chatContext, setActiveSession }: ChatPanelInnerProps) => {
|
const ChatPanelInner = ({ sessionId, model, scoped, sandboxed, cwdParam, promptPrefix, chatContext, setActiveSession, onTurnComplete }: ChatPanelInnerProps) => {
|
||||||
const chat = usePiChat(sessionId, model, { replaceUrl: false, projectScoped: scoped, ...chatContext });
|
const chat = usePiChat(sessionId, model, { replaceUrl: false, projectScoped: scoped, onTurnComplete, ...chatContext });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveSession(chat.sessionId);
|
setActiveSession(chat.sessionId);
|
||||||
@@ -58,11 +60,17 @@ export const ChatPanelWrapper = () => {
|
|||||||
|
|
||||||
const [selection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
const [selection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
||||||
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
|
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
|
||||||
|
const [, setPreviewRefresh] = usePanelChannel<number>('preview:refresh', 0);
|
||||||
|
|
||||||
|
const onTurnComplete = useCallback((hadToolCalls: boolean) => {
|
||||||
|
if (hadToolCalls) setPreviewRefresh(Date.now());
|
||||||
|
}, [setPreviewRefresh]);
|
||||||
|
|
||||||
const cwdParam = scoped ? { root, path: cwd } : undefined;
|
const cwdParam = scoped ? { root, path: cwd } : undefined;
|
||||||
|
|
||||||
const sessionId = selection?.sessionId ?? undefined;
|
const isCurrentWorkspace = !selection?.workspaceId || selection.workspaceId === workspaceId;
|
||||||
const model = selection?.model ?? undefined;
|
const sessionId = isCurrentWorkspace ? selection?.sessionId ?? undefined : undefined;
|
||||||
|
const model = isCurrentWorkspace ? selection?.model ?? undefined : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatPanelInner
|
<ChatPanelInner
|
||||||
@@ -75,6 +83,7 @@ export const ChatPanelWrapper = () => {
|
|||||||
promptPrefix={promptPrefix}
|
promptPrefix={promptPrefix}
|
||||||
chatContext={chatContext}
|
chatContext={chatContext}
|
||||||
setActiveSession={setActiveSession}
|
setActiveSession={setActiveSession}
|
||||||
|
onTurnComplete={onTurnComplete}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Globe, RefreshCw, Square, Play } from 'lucide-react';
|
|||||||
import { usePreview } from './PreviewContext';
|
import { usePreview } from './PreviewContext';
|
||||||
|
|
||||||
export const PreviewHeader = () => {
|
export const PreviewHeader = () => {
|
||||||
const { slug, url, port, stopped, isSuperAdmin, refresh, stopServer, restartServer } = usePreview();
|
const { slug, url, port, stopped, isSuperAdmin, stopServer, restartServer } = usePreview();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -12,7 +12,7 @@ export const PreviewHeader = () => {
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={refresh}
|
onClick={restartServer}
|
||||||
className="p-0.5 rounded hover:bg-black/10 transition-colors cursor-pointer shrink-0"
|
className="p-0.5 rounded hover:bg-black/10 transition-colors cursor-pointer shrink-0"
|
||||||
title="Refresh"
|
title="Refresh"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||||
import { useWorkspace } from '../../components/Workspace';
|
import { useWorkspace } from '../../components/Workspace';
|
||||||
import type { ProjectDefinition } from '../../components/Workspace';
|
import type { ProjectDefinition } from '../../components/Workspace';
|
||||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||||
@@ -12,7 +13,7 @@ type DevServerStatus = { running: boolean; url?: string; port?: number };
|
|||||||
|
|
||||||
type PreviewProviderProps = { panelId: string; children: ReactNode };
|
type PreviewProviderProps = { panelId: string; children: ReactNode };
|
||||||
|
|
||||||
export const PreviewProvider = ({ children }: PreviewProviderProps) => {
|
export const PreviewProvider = ({ panelId, children }: PreviewProviderProps) => {
|
||||||
const { cwd } = useWorkspace();
|
const { cwd } = useWorkspace();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -58,10 +59,15 @@ export const PreviewProvider = ({ children }: PreviewProviderProps) => {
|
|||||||
setStopped(true);
|
setStopped(true);
|
||||||
}, [slug, client]);
|
}, [slug, client]);
|
||||||
|
|
||||||
const restartServer = useCallback(() => {
|
const restartServer = useCallback(async () => {
|
||||||
if (!slug) return;
|
if (!slug) return;
|
||||||
|
try {
|
||||||
|
await client.post('/dev-server/stop', { slug });
|
||||||
|
} catch {
|
||||||
|
// ignore — server may not be running
|
||||||
|
}
|
||||||
startServer(slug);
|
startServer(slug);
|
||||||
}, [slug, startServer]);
|
}, [slug, client, startServer]);
|
||||||
|
|
||||||
const refresh = useCallback(() => setIframeKey((k) => k + 1), []);
|
const refresh = useCallback(() => setIframeKey((k) => k + 1), []);
|
||||||
|
|
||||||
@@ -115,6 +121,12 @@ export const PreviewProvider = ({ children }: PreviewProviderProps) => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [slug, url]);
|
}, [slug, url]);
|
||||||
|
|
||||||
|
// Listen for preview:refresh signal from chat panel (triggers after tool-call turns)
|
||||||
|
const [refreshSignal] = usePanelChannel<number>('preview:refresh', 0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (refreshSignal && slug && url) restartServer();
|
||||||
|
}, [refreshSignal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreviewContext
|
<PreviewContext
|
||||||
value={{
|
value={{
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ type UsePiChatOptions = {
|
|||||||
projectScoped?: boolean;
|
projectScoped?: boolean;
|
||||||
context?: string;
|
context?: string;
|
||||||
contextId?: string;
|
contextId?: string;
|
||||||
|
onTurnComplete?: (hadToolCalls: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function usePiChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) {
|
export function usePiChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) {
|
||||||
const { replaceUrl = true, storage, resourceChatDir, taskInfo, projectScoped, context, contextId } = options ?? {};
|
const { replaceUrl = true, storage, resourceChatDir, taskInfo, projectScoped, context, contextId, onTurnComplete } = options ?? {};
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [streamingText, setStreamingText] = useState('');
|
const [streamingText, setStreamingText] = useState('');
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
@@ -55,6 +56,9 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
||||||
const saveTimerRef = useRef<number | null>(null);
|
const saveTimerRef = useRef<number | null>(null);
|
||||||
|
const toolCallsInTurnRef = useRef(false);
|
||||||
|
const onTurnCompleteRef = useRef(onTurnComplete);
|
||||||
|
onTurnCompleteRef.current = onTurnComplete;
|
||||||
|
|
||||||
const sessionFilter = context ? { context, contextId } : undefined;
|
const sessionFilter = context ? { context, contextId } : undefined;
|
||||||
const { getSession, saveMessages, invalidate: invalidateSessions } = useChatSessions(sessionFilter);
|
const { getSession, saveMessages, invalidate: invalidateSessions } = useChatSessions(sessionFilter);
|
||||||
@@ -104,6 +108,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tool:start':
|
case 'tool:start':
|
||||||
|
toolCallsInTurnRef.current = true;
|
||||||
setMessages((prev) => [
|
setMessages((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -125,7 +130,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'result':
|
case 'result': {
|
||||||
commitStreaming();
|
commitStreaming();
|
||||||
setMessages((prev) => [
|
setMessages((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -136,7 +141,11 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
]);
|
]);
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
invalidateSessions();
|
invalidateSessions();
|
||||||
|
const hadTools = toolCallsInTurnRef.current;
|
||||||
|
toolCallsInTurnRef.current = false;
|
||||||
|
onTurnCompleteRef.current?.(hadTools);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'sync:messages':
|
case 'sync:messages':
|
||||||
sessionIdRef.current = msg.sessionId;
|
sessionIdRef.current = msg.sessionId;
|
||||||
@@ -281,6 +290,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
streamingRef.current = '';
|
streamingRef.current = '';
|
||||||
setStreamingText('');
|
setStreamingText('');
|
||||||
|
toolCallsInTurnRef.current = false;
|
||||||
|
|
||||||
// Parse dataUrls into { mediaType, data } for the server
|
// Parse dataUrls into { mediaType, data } for the server
|
||||||
const imageData = images
|
const imageData = images
|
||||||
|
|||||||
Reference in New Issue
Block a user