better chat ui
This commit is contained in:
@@ -22,7 +22,7 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
setSelected({ id: 'new' });
|
setSelected({ id: `new:${Date.now()}` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|||||||
+622
-297
File diff suppressed because it is too large
Load Diff
@@ -152,3 +152,92 @@ export type ModelInfo = {
|
|||||||
reasoning?: boolean;
|
reasoning?: boolean;
|
||||||
images?: boolean;
|
images?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Pi-native JSONL types ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type JnlSessionHeader = {
|
||||||
|
type: 'session';
|
||||||
|
version: 3;
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
cwd: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlEntryBase = {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
parentId?: string;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlTextContent = {
|
||||||
|
type: 'text';
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlToolCall = {
|
||||||
|
type: 'tool_use';
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
input: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlUserMessage = JnlEntryBase & {
|
||||||
|
type: 'message';
|
||||||
|
message: {
|
||||||
|
role: 'user';
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlAssistantMessage = JnlEntryBase & {
|
||||||
|
type: 'message';
|
||||||
|
message: {
|
||||||
|
role: 'assistant';
|
||||||
|
content: Array<JnlTextContent | JnlToolCall>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlToolResultMessage = JnlEntryBase & {
|
||||||
|
type: 'message';
|
||||||
|
message: {
|
||||||
|
role: 'toolResult';
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
content: Array<JnlTextContent>;
|
||||||
|
isError?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlMessageEntry = JnlUserMessage | JnlAssistantMessage | JnlToolResultMessage;
|
||||||
|
|
||||||
|
export type JnlSessionInfoEntry = JnlEntryBase & {
|
||||||
|
type: 'session_info';
|
||||||
|
name: string;
|
||||||
|
officer?: {
|
||||||
|
cost: MessageCost;
|
||||||
|
model: string;
|
||||||
|
groupSlug?: string | null;
|
||||||
|
messageCount: number;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JnlEntry = JnlMessageEntry | JnlSessionInfoEntry;
|
||||||
|
|
||||||
|
// ── Session Index ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type SessionIndexEntry = {
|
||||||
|
file: string;
|
||||||
|
title: string;
|
||||||
|
model: string;
|
||||||
|
cwd: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
messageCount: number;
|
||||||
|
cost: MessageCost;
|
||||||
|
groupSlug?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionIndex = Record<string, SessionIndexEntry>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||||
@@ -83,6 +82,7 @@ function SessionChat({ sessionId, model }: SessionChatProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EmbeddableChat
|
<EmbeddableChat
|
||||||
|
chat={chat}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
initialModel={model ?? undefined}
|
initialModel={model ?? undefined}
|
||||||
className="flex-1 min-h-0"
|
className="flex-1 min-h-0"
|
||||||
@@ -94,15 +94,8 @@ function SessionChat({ sessionId, model }: SessionChatProps) {
|
|||||||
function NewChat() {
|
function NewChat() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const locationState = location.state as ChatLocationState;
|
const locationState = location.state as ChatLocationState;
|
||||||
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
||||||
|
|
||||||
const chat = usePiChat();
|
const chat = usePiChat(undefined, locationState?.model);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (chat.sessionId) {
|
|
||||||
setSelected({ id: chat.sessionId, model: chat.model });
|
|
||||||
}
|
|
||||||
}, [chat.sessionId]);
|
|
||||||
|
|
||||||
const initialMessage = locationState?.initialMessage
|
const initialMessage = locationState?.initialMessage
|
||||||
? {
|
? {
|
||||||
@@ -122,6 +115,7 @@ function NewChat() {
|
|||||||
onDelete={undefined}
|
onDelete={undefined}
|
||||||
/>
|
/>
|
||||||
<EmbeddableChat
|
<EmbeddableChat
|
||||||
|
chat={chat}
|
||||||
sessionId={undefined}
|
sessionId={undefined}
|
||||||
initialModel={locationState?.model ?? undefined}
|
initialModel={locationState?.model ?? undefined}
|
||||||
initialMessage={initialMessage}
|
initialMessage={initialMessage}
|
||||||
@@ -133,16 +127,6 @@ function NewChat() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewChatPanel() {
|
|
||||||
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
||||||
|
|
||||||
if (selected && selected.id !== 'new') {
|
|
||||||
return <SessionChat key={selected.id} sessionId={selected.id} model={selected.model} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <NewChat key="new" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatDetailPanel = () => {
|
export const ChatDetailPanel = () => {
|
||||||
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
||||||
|
|
||||||
@@ -154,8 +138,8 @@ export const ChatDetailPanel = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.id === 'new') {
|
if (selected.id.startsWith('new')) {
|
||||||
return <NewChatPanel key="new" />;
|
return <NewChat key={selected.id} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SessionChat key={selected.id} sessionId={selected.id} model={selected.model} />;
|
return <SessionChat key={selected.id} sessionId={selected.id} model={selected.model} />;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export const SessionList = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelected({ id: 'new' });
|
setSelected({ id: `new:${Date.now()}` });
|
||||||
window.history.replaceState(null, '', '/chat/new');
|
window.history.replaceState(null, '', '/chat/new');
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
|
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
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 { getSession, saveMessages } = useChatSessions();
|
const { getSession, saveMessages, invalidate: invalidateSessions } = useChatSessions();
|
||||||
|
|
||||||
const token = localStorage.getItem('BEARER_TOKEN');
|
const token = localStorage.getItem('BEARER_TOKEN');
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
@@ -124,6 +124,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
|
invalidateSessions();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'sync:messages':
|
case 'sync:messages':
|
||||||
@@ -148,6 +149,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
return { role: 'assistant', text: '' }; // Fallback
|
return { role: 'assistant', text: '' }; // Fallback
|
||||||
});
|
});
|
||||||
setMessages(chatMessages);
|
setMessages(chatMessages);
|
||||||
|
if (chatMessages.length > 0) setHasStarted(true);
|
||||||
setIsGenerating(msg.isGenerating);
|
setIsGenerating(msg.isGenerating);
|
||||||
if (msg.streamingText) {
|
if (msg.streamingText) {
|
||||||
streamingRef.current = msg.streamingText;
|
streamingRef.current = msg.streamingText;
|
||||||
@@ -180,7 +182,10 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
sessionIdRef.current = sid;
|
sessionIdRef.current = sid;
|
||||||
setSessionId(sid);
|
setSessionId(sid);
|
||||||
}
|
}
|
||||||
if (msgs.length > 0) setMessages(msgs);
|
if (msgs.length > 0) {
|
||||||
|
setMessages(msgs);
|
||||||
|
setHasStarted(true);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
return;
|
return;
|
||||||
@@ -208,6 +213,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
return { role: 'assistant', text: '' }; // Fallback
|
return { role: 'assistant', text: '' }; // Fallback
|
||||||
});
|
});
|
||||||
setMessages(chatMessages);
|
setMessages(chatMessages);
|
||||||
|
setHasStarted(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export function useChatSessions() {
|
|||||||
return client.get<{ results: SessionEntry[] }>(`/pi/sessions/search?q=${encodeURIComponent(query)}`);
|
return client.get<{ results: SessionEntry[] }>(`/pi/sessions/search?q=${encodeURIComponent(query)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidate() {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions,
|
sessions,
|
||||||
getSession,
|
getSession,
|
||||||
@@ -46,6 +50,7 @@ export function useChatSessions() {
|
|||||||
renameSession,
|
renameSession,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
searchSessions,
|
searchSessions,
|
||||||
|
invalidate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user