better chat ui
This commit is contained in:
@@ -22,7 +22,7 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isNew) {
|
||||
setSelected({ id: 'new' });
|
||||
setSelected({ id: `new:${Date.now()}` });
|
||||
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;
|
||||
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 { Trash2 } from 'lucide-react';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
@@ -83,6 +82,7 @@ function SessionChat({ sessionId, model }: SessionChatProps) {
|
||||
}}
|
||||
/>
|
||||
<EmbeddableChat
|
||||
chat={chat}
|
||||
sessionId={sessionId}
|
||||
initialModel={model ?? undefined}
|
||||
className="flex-1 min-h-0"
|
||||
@@ -94,15 +94,8 @@ function SessionChat({ sessionId, model }: SessionChatProps) {
|
||||
function NewChat() {
|
||||
const location = useLocation();
|
||||
const locationState = location.state as ChatLocationState;
|
||||
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
||||
|
||||
const chat = usePiChat();
|
||||
|
||||
useEffect(() => {
|
||||
if (chat.sessionId) {
|
||||
setSelected({ id: chat.sessionId, model: chat.model });
|
||||
}
|
||||
}, [chat.sessionId]);
|
||||
const chat = usePiChat(undefined, locationState?.model);
|
||||
|
||||
const initialMessage = locationState?.initialMessage
|
||||
? {
|
||||
@@ -122,6 +115,7 @@ function NewChat() {
|
||||
onDelete={undefined}
|
||||
/>
|
||||
<EmbeddableChat
|
||||
chat={chat}
|
||||
sessionId={undefined}
|
||||
initialModel={locationState?.model ?? undefined}
|
||||
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 = () => {
|
||||
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
||||
|
||||
@@ -154,8 +138,8 @@ export const ChatDetailPanel = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (selected.id === 'new') {
|
||||
return <NewChatPanel key="new" />;
|
||||
if (selected.id.startsWith('new')) {
|
||||
return <NewChat key={selected.id} />;
|
||||
}
|
||||
|
||||
return <SessionChat key={selected.id} sessionId={selected.id} model={selected.model} />;
|
||||
|
||||
@@ -132,7 +132,7 @@ export const SessionList = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelected({ id: 'new' });
|
||||
setSelected({ id: `new:${Date.now()}` });
|
||||
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"
|
||||
|
||||
@@ -46,7 +46,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
||||
const saveTimerRef = useRef<number | null>(null);
|
||||
|
||||
const { getSession, saveMessages } = useChatSessions();
|
||||
const { getSession, saveMessages, invalidate: invalidateSessions } = useChatSessions();
|
||||
|
||||
const token = localStorage.getItem('BEARER_TOKEN');
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -124,6 +124,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
},
|
||||
]);
|
||||
setIsGenerating(false);
|
||||
invalidateSessions();
|
||||
break;
|
||||
|
||||
case 'sync:messages':
|
||||
@@ -148,6 +149,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
return { role: 'assistant', text: '' }; // Fallback
|
||||
});
|
||||
setMessages(chatMessages);
|
||||
if (chatMessages.length > 0) setHasStarted(true);
|
||||
setIsGenerating(msg.isGenerating);
|
||||
if (msg.streamingText) {
|
||||
streamingRef.current = msg.streamingText;
|
||||
@@ -180,7 +182,10 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
sessionIdRef.current = sid;
|
||||
setSessionId(sid);
|
||||
}
|
||||
if (msgs.length > 0) setMessages(msgs);
|
||||
if (msgs.length > 0) {
|
||||
setMessages(msgs);
|
||||
setHasStarted(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
@@ -208,6 +213,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
return { role: 'assistant', text: '' }; // Fallback
|
||||
});
|
||||
setMessages(chatMessages);
|
||||
setHasStarted(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
@@ -39,6 +39,10 @@ export function useChatSessions() {
|
||||
return client.get<{ results: SessionEntry[] }>(`/pi/sessions/search?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
|
||||
function invalidate() {
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
}
|
||||
|
||||
return {
|
||||
sessions,
|
||||
getSession,
|
||||
@@ -46,6 +50,7 @@ export function useChatSessions() {
|
||||
renameSession,
|
||||
deleteSession,
|
||||
searchSessions,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user