|
|
|
@@ -1,17 +1,14 @@
|
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
|
import { useLocation } from 'react-router';
|
|
|
|
|
import { Trash2, Archive } from 'lucide-react';
|
|
|
|
|
import { Trash2 } from 'lucide-react';
|
|
|
|
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
|
|
|
|
import { useChatSessions } from '@/state/useChatSessions';
|
|
|
|
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels, useVisiblePiMonoModels } from '@/state/useModels';
|
|
|
|
|
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
|
|
|
|
import { useOpenCode } from '@/Screens/Dashboard/Chat/useOpenCode';
|
|
|
|
|
import { usePiMono } from '@/Screens/Dashboard/Chat/usePiMono';
|
|
|
|
|
import { useVisiblePiModels } from '@/state/useModels';
|
|
|
|
|
import { usePi } from '@/Screens/Dashboard/Chat/usePi';
|
|
|
|
|
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
|
|
|
|
|
|
|
|
|
export type SelectedSession = {
|
|
|
|
|
id: string;
|
|
|
|
|
provider: 'claude' | 'opencode' | 'pi-mono';
|
|
|
|
|
model?: string | null;
|
|
|
|
|
} | null;
|
|
|
|
|
|
|
|
|
@@ -27,95 +24,50 @@ type ChatLocationState = {
|
|
|
|
|
} | null;
|
|
|
|
|
|
|
|
|
|
type DetailBarProps = {
|
|
|
|
|
provider: 'claude' | 'opencode' | 'pi-mono';
|
|
|
|
|
sessionTitle: string | undefined;
|
|
|
|
|
isConnected: boolean;
|
|
|
|
|
isGenerating: boolean;
|
|
|
|
|
onArchive: (() => void) | undefined;
|
|
|
|
|
onDelete: (() => void) | undefined;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DetailBar = ({
|
|
|
|
|
provider,
|
|
|
|
|
sessionTitle,
|
|
|
|
|
isConnected,
|
|
|
|
|
isGenerating,
|
|
|
|
|
onArchive,
|
|
|
|
|
onDelete,
|
|
|
|
|
}: DetailBarProps) => (
|
|
|
|
|
<div className="shrink-0 flex items-center px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60">
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{provider === 'claude' && onArchive && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onArchive}
|
|
|
|
|
className="p-1 text-duck-dark/40 dark:text-foreground/40 hover:text-duck-teal transition-colors cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<Archive className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{onDelete && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onDelete}
|
|
|
|
|
className="p-1 text-duck-dark/40 dark:text-foreground/40 hover:text-red-500 transition-colors cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
function DetailBar({ sessionTitle, isConnected, isGenerating, onDelete }: DetailBarProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="shrink-0 flex items-center px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60">
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{onDelete && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onDelete}
|
|
|
|
|
className="p-1 text-duck-dark/40 dark:text-foreground/40 hover:text-red-500 transition-colors cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0 text-center text-sm font-medium text-duck-dark/70 dark:text-foreground/70 truncate px-3">
|
|
|
|
|
{sessionTitle ?? 'New chat'}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2 text-xs text-duck-dark/50 dark:text-foreground/50">
|
|
|
|
|
{!isConnected ? (
|
|
|
|
|
<span className="inline-block h-2 w-2 rounded-full bg-red-500" />
|
|
|
|
|
) : isGenerating ? (
|
|
|
|
|
<span className="inline-block h-2 w-2 rounded-full bg-duck-orange animate-pulse" />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="inline-block h-2 w-2 rounded-full bg-green-500" />
|
|
|
|
|
)}
|
|
|
|
|
<span>{!isConnected ? 'Disconnected' : isGenerating ? 'Working...' : ''}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0 text-center text-sm font-medium text-duck-dark/70 dark:text-foreground/70 truncate px-3">
|
|
|
|
|
{sessionTitle ?? 'New chat'}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2 text-xs text-duck-dark/50 dark:text-foreground/50">
|
|
|
|
|
{!isConnected ? (
|
|
|
|
|
<span className="inline-block h-2 w-2 rounded-full bg-red-500" />
|
|
|
|
|
) : isGenerating ? (
|
|
|
|
|
<span className="inline-block h-2 w-2 rounded-full bg-duck-orange animate-pulse" />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="inline-block h-2 w-2 rounded-full bg-green-500" />
|
|
|
|
|
)}
|
|
|
|
|
<span>{!isConnected ? 'Disconnected' : isGenerating ? 'Working...' : ''}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type InnerProps = {
|
|
|
|
|
type SessionChatProps = {
|
|
|
|
|
sessionId: string;
|
|
|
|
|
model?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ClaudeInner = ({ sessionId, model }: InnerProps) => {
|
|
|
|
|
const chat = useClaude(sessionId, model, { replaceUrl: false });
|
|
|
|
|
const models = useVisibleClaudeModels();
|
|
|
|
|
const { sessions, archiveSession, deleteSession } = useChatSessions();
|
|
|
|
|
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
const sessionTitle = sessions.find((s) => s.id === sessionId)?.title;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<DetailBar
|
|
|
|
|
provider="claude"
|
|
|
|
|
sessionTitle={sessionTitle}
|
|
|
|
|
isConnected={chat.isConnected}
|
|
|
|
|
isGenerating={chat.isGenerating}
|
|
|
|
|
onArchive={async () => {
|
|
|
|
|
await archiveSession('claude', sessionId);
|
|
|
|
|
setSelected(null);
|
|
|
|
|
window.history.replaceState(null, '', '/chat');
|
|
|
|
|
}}
|
|
|
|
|
onDelete={async () => {
|
|
|
|
|
await deleteSession('claude', sessionId);
|
|
|
|
|
setSelected(null);
|
|
|
|
|
window.history.replaceState(null, '', '/chat');
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<EmbeddableChat chat={chat} availableModels={models} className="flex-1 min-h-0" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const OpenCodeInner = ({ sessionId, model }: InnerProps) => {
|
|
|
|
|
const chat = useOpenCode(sessionId, model, { replaceUrl: false });
|
|
|
|
|
const models = useVisibleOpenCodeModels();
|
|
|
|
|
function SessionChat({ sessionId, model }: SessionChatProps) {
|
|
|
|
|
const chat = usePi(sessionId, model, { replaceUrl: false });
|
|
|
|
|
const models = useVisiblePiModels();
|
|
|
|
|
const { sessions, deleteSession } = useChatSessions();
|
|
|
|
|
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
const sessionTitle = sessions.find((s) => s.id === sessionId)?.title;
|
|
|
|
@@ -123,13 +75,11 @@ const OpenCodeInner = ({ sessionId, model }: InnerProps) => {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<DetailBar
|
|
|
|
|
provider="opencode"
|
|
|
|
|
sessionTitle={sessionTitle}
|
|
|
|
|
isConnected={chat.isConnected}
|
|
|
|
|
isGenerating={chat.isGenerating}
|
|
|
|
|
onArchive={undefined}
|
|
|
|
|
onDelete={async () => {
|
|
|
|
|
await deleteSession('opencode', sessionId);
|
|
|
|
|
await deleteSession(sessionId);
|
|
|
|
|
setSelected(null);
|
|
|
|
|
window.history.replaceState(null, '', '/chat');
|
|
|
|
|
}}
|
|
|
|
@@ -137,45 +87,19 @@ const OpenCodeInner = ({ sessionId, model }: InnerProps) => {
|
|
|
|
|
<EmbeddableChat chat={chat} availableModels={models} className="flex-1 min-h-0" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PiMonoInner = ({ sessionId, model }: InnerProps) => {
|
|
|
|
|
const chat = usePiMono(sessionId, model, { replaceUrl: false });
|
|
|
|
|
const models = useVisiblePiMonoModels();
|
|
|
|
|
const { sessions, deleteSession } = useChatSessions();
|
|
|
|
|
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
const sessionTitle = sessions.find((s) => s.id === sessionId)?.title;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<DetailBar
|
|
|
|
|
provider="pi-mono"
|
|
|
|
|
sessionTitle={sessionTitle}
|
|
|
|
|
isConnected={chat.isConnected}
|
|
|
|
|
isGenerating={chat.isGenerating}
|
|
|
|
|
onArchive={undefined}
|
|
|
|
|
onDelete={async () => {
|
|
|
|
|
await deleteSession('pi-mono', sessionId);
|
|
|
|
|
setSelected(null);
|
|
|
|
|
window.history.replaceState(null, '', '/chat');
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<EmbeddableChat chat={chat} availableModels={models} className="flex-1 min-h-0" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const NewClaudeInner = () => {
|
|
|
|
|
function NewChat() {
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const locationState = location.state as ChatLocationState;
|
|
|
|
|
const initialSentRef = useRef(false);
|
|
|
|
|
const chat = useClaude();
|
|
|
|
|
const models = useVisibleClaudeModels();
|
|
|
|
|
const chat = usePi();
|
|
|
|
|
const models = useVisiblePiModels();
|
|
|
|
|
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (chat.sessionId) {
|
|
|
|
|
setSelected({ id: chat.sessionId, provider: 'claude', model: chat.model });
|
|
|
|
|
setSelected({ id: chat.sessionId, model: chat.model });
|
|
|
|
|
}
|
|
|
|
|
}, [chat.sessionId]);
|
|
|
|
|
|
|
|
|
@@ -196,11 +120,9 @@ const NewClaudeInner = () => {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<DetailBar
|
|
|
|
|
provider="claude"
|
|
|
|
|
sessionTitle={undefined}
|
|
|
|
|
isConnected={chat.isConnected}
|
|
|
|
|
isGenerating={chat.isGenerating}
|
|
|
|
|
onArchive={undefined}
|
|
|
|
|
onDelete={undefined}
|
|
|
|
|
/>
|
|
|
|
|
<EmbeddableChat
|
|
|
|
@@ -212,120 +134,19 @@ const NewClaudeInner = () => {
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NewOpenCodeInner = () => {
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const locationState = location.state as ChatLocationState;
|
|
|
|
|
const initialSentRef = useRef(false);
|
|
|
|
|
const chat = useOpenCode();
|
|
|
|
|
const models = useVisibleOpenCodeModels();
|
|
|
|
|
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (chat.sessionId) {
|
|
|
|
|
setSelected({ id: chat.sessionId, provider: 'opencode', model: chat.model });
|
|
|
|
|
}
|
|
|
|
|
}, [chat.sessionId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!locationState || initialSentRef.current || !chat.isConnected) return;
|
|
|
|
|
if (locationState.prefillInput) {
|
|
|
|
|
initialSentRef.current = true;
|
|
|
|
|
window.history.replaceState({}, '', location.pathname);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!locationState.initialMessage) return;
|
|
|
|
|
initialSentRef.current = true;
|
|
|
|
|
if (locationState.model) chat.setSelectedModel(locationState.model);
|
|
|
|
|
chat.sendPrompt(locationState.initialMessage, locationState.attachmentIds, locationState.images);
|
|
|
|
|
window.history.replaceState({}, '', location.pathname);
|
|
|
|
|
}, [chat.isConnected, location.state]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<DetailBar
|
|
|
|
|
provider="opencode"
|
|
|
|
|
sessionTitle={undefined}
|
|
|
|
|
isConnected={chat.isConnected}
|
|
|
|
|
isGenerating={chat.isGenerating}
|
|
|
|
|
onArchive={undefined}
|
|
|
|
|
onDelete={undefined}
|
|
|
|
|
/>
|
|
|
|
|
<EmbeddableChat
|
|
|
|
|
chat={chat}
|
|
|
|
|
availableModels={models}
|
|
|
|
|
defaultInput={locationState?.prefillInput ?? ''}
|
|
|
|
|
className="flex-1 min-h-0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const NewPiMonoInner = () => {
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const locationState = location.state as ChatLocationState;
|
|
|
|
|
const initialSentRef = useRef(false);
|
|
|
|
|
const chat = usePiMono();
|
|
|
|
|
const models = useVisiblePiMonoModels();
|
|
|
|
|
const [, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (chat.sessionId) {
|
|
|
|
|
setSelected({ id: chat.sessionId, provider: 'pi-mono', model: chat.model });
|
|
|
|
|
}
|
|
|
|
|
}, [chat.sessionId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!locationState || initialSentRef.current || !chat.isConnected) return;
|
|
|
|
|
if (locationState.prefillInput) {
|
|
|
|
|
initialSentRef.current = true;
|
|
|
|
|
window.history.replaceState({}, '', location.pathname);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!locationState.initialMessage) return;
|
|
|
|
|
initialSentRef.current = true;
|
|
|
|
|
if (locationState.model) chat.setSelectedModel(locationState.model);
|
|
|
|
|
chat.sendPrompt(locationState.initialMessage, locationState.attachmentIds, locationState.images);
|
|
|
|
|
window.history.replaceState({}, '', location.pathname);
|
|
|
|
|
}, [chat.isConnected, location.state]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<DetailBar
|
|
|
|
|
provider="pi-mono"
|
|
|
|
|
sessionTitle={undefined}
|
|
|
|
|
isConnected={chat.isConnected}
|
|
|
|
|
isGenerating={chat.isGenerating}
|
|
|
|
|
onArchive={undefined}
|
|
|
|
|
onDelete={undefined}
|
|
|
|
|
/>
|
|
|
|
|
<EmbeddableChat
|
|
|
|
|
chat={chat}
|
|
|
|
|
availableModels={models}
|
|
|
|
|
defaultInput={locationState?.prefillInput ?? ''}
|
|
|
|
|
className="flex-1 min-h-0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type NewChatPanelProps = {
|
|
|
|
|
initialProvider?: 'claude' | 'opencode' | 'pi-mono';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const NewChatPanel = ({ initialProvider = 'pi-mono' }: NewChatPanelProps) => {
|
|
|
|
|
function NewChatPanel() {
|
|
|
|
|
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
|
|
|
|
|
// Once a session is created, the inner component updates selected via the channel
|
|
|
|
|
if (selected && selected.id !== 'new') {
|
|
|
|
|
return <PiMonoInner key={selected.id} sessionId={selected.id} model={selected.model} />;
|
|
|
|
|
return <SessionChat key={selected.id} sessionId={selected.id} model={selected.model} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <NewPiMonoInner key="new-pi-mono" />;
|
|
|
|
|
};
|
|
|
|
|
return <NewChat key="new" />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ChatDetailPanel = () => {
|
|
|
|
|
export function ChatDetailPanel() {
|
|
|
|
|
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
|
|
|
|
|
|
|
|
|
|
if (!selected) {
|
|
|
|
@@ -337,8 +158,8 @@ export const ChatDetailPanel = () => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selected.id === 'new') {
|
|
|
|
|
return <NewChatPanel key="new" initialProvider={selected.provider} />;
|
|
|
|
|
return <NewChatPanel key="new" />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <PiMonoInner key={selected.id} sessionId={selected.id} model={selected.model} />;
|
|
|
|
|
};
|
|
|
|
|
return <SessionChat key={selected.id} sessionId={selected.id} model={selected.model} />;
|
|
|
|
|
}
|
|
|
|
|