170 lines
6.0 KiB
TypeScript
170 lines
6.0 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog';
|
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
import { cardStyle } from '@/components/Card';
|
|
import type { TaskInfo } from 'widgets/Chat';
|
|
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
|
|
import { useOpenCode } from '@/Screens/Dashboard/Chat/useOpenCode';
|
|
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
|
|
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
|
|
import { useSettings } from '@/state/useSettings';
|
|
import type { TaskSummary } from 'widgets/FileBrowser';
|
|
|
|
const playDing = () => {
|
|
const ctx = new AudioContext();
|
|
const t = ctx.currentTime;
|
|
|
|
const play = (freq: number, start: number, dur: number) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(freq, t + start);
|
|
gain.gain.setValueAtTime(0.8, t + start);
|
|
gain.gain.setValueAtTime(0.8, t + start + dur * 0.6);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + start + dur);
|
|
osc.connect(gain).connect(ctx.destination);
|
|
osc.start(t + start);
|
|
osc.stop(t + start + dur);
|
|
};
|
|
|
|
play(880, 0, 0.4);
|
|
play(1100, 0.25, 0.4);
|
|
play(1320, 0.5, 0.6);
|
|
|
|
setTimeout(() => ctx.close(), 1500);
|
|
};
|
|
|
|
type InnerProps = {
|
|
defaultInput: string;
|
|
cwd: { root?: string; path: string };
|
|
initialModel: string | null;
|
|
onProviderChange: (p: 'claude' | 'opencode') => void;
|
|
};
|
|
|
|
const ClaudeInner = ({
|
|
defaultInput,
|
|
cwd,
|
|
initialModel,
|
|
onProviderChange,
|
|
taskInfo,
|
|
}: InnerProps & { taskInfo: TaskInfo }) => {
|
|
const chat = useClaude(undefined, initialModel, { replaceUrl: false, taskInfo });
|
|
const models = useVisibleClaudeModels();
|
|
|
|
const wasGenerating = useRef(false);
|
|
useEffect(() => {
|
|
if (wasGenerating.current && !chat.isGenerating) playDing();
|
|
wasGenerating.current = chat.isGenerating;
|
|
}, [chat.isGenerating]);
|
|
|
|
return (
|
|
<EmbeddableChat
|
|
chat={chat}
|
|
provider="claude"
|
|
availableModels={models}
|
|
onProviderChange={onProviderChange}
|
|
defaultInput={defaultInput}
|
|
cwd={cwd}
|
|
className="flex-1 min-h-0"
|
|
/>
|
|
);
|
|
};
|
|
|
|
const OpenCodeInner = ({
|
|
defaultInput,
|
|
cwd,
|
|
initialModel,
|
|
onProviderChange,
|
|
taskInfo,
|
|
}: InnerProps & { taskInfo: TaskInfo }) => {
|
|
const chat = useOpenCode(undefined, initialModel, { replaceUrl: false, taskInfo });
|
|
const models = useVisibleOpenCodeModels();
|
|
|
|
const wasGenerating = useRef(false);
|
|
useEffect(() => {
|
|
if (wasGenerating.current && !chat.isGenerating) playDing();
|
|
wasGenerating.current = chat.isGenerating;
|
|
}, [chat.isGenerating]);
|
|
|
|
return (
|
|
<EmbeddableChat
|
|
chat={chat}
|
|
provider="opencode"
|
|
availableModels={models}
|
|
onProviderChange={onProviderChange}
|
|
defaultInput={defaultInput}
|
|
cwd={cwd}
|
|
className="flex-1 min-h-0"
|
|
/>
|
|
);
|
|
};
|
|
|
|
type TaskRunnerModalProps = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
task: TaskSummary;
|
|
entryName: string;
|
|
entryType: 'file' | 'directory';
|
|
cwd: { root?: string; path: string };
|
|
};
|
|
|
|
export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryType, cwd }: TaskRunnerModalProps) => {
|
|
const { settings } = useSettings();
|
|
const taskSettings = settings.tasks;
|
|
const [provider, setProvider] = useState<'claude' | 'opencode'>(taskSettings.defaultProvider);
|
|
const defaultInput = `Read the task instructions at ${task.filePath} and execute them on the ${entryType}: ${entryName}`;
|
|
const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName, entryType };
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogPortal>
|
|
<DialogOverlay className="z-[700] bg-black/60 backdrop-blur-sm" />
|
|
<DialogPrimitive.Content
|
|
onOpenAutoFocus={(ev) => ev.preventDefault()}
|
|
className="fixed left-[50%] top-[50%] z-[700] translate-x-[-50%] translate-y-[-50%] flex flex-col overflow-hidden rounded-xl border-2 border-duck-dark/30 shadow-2xl w-[90vw] max-w-3xl h-[80vh] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
|
style={cardStyle({
|
|
backgroundColor: 'rgba(255, 255, 255, 0.97)',
|
|
backgroundImage: `
|
|
linear-gradient(to right, rgba(20, 83, 45, 0.04) 1px, transparent 1px),
|
|
linear-gradient(to bottom, rgba(20, 83, 45, 0.04) 1px, transparent 1px)
|
|
`,
|
|
})}
|
|
>
|
|
{/* Header */}
|
|
<div className="shrink-0 flex items-center gap-3 px-5 py-3 border-b border-duck-dark/10 bg-white/60">
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-sm font-semibold text-duck-dark truncate block">{task.name}</span>
|
|
<span className="text-xs text-duck-dark/50 truncate block">{entryName}</span>
|
|
</div>
|
|
<DialogPrimitive.Close className="p-1.5 rounded-md text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5 transition-colors cursor-pointer">
|
|
<X className="h-4 w-4" />
|
|
</DialogPrimitive.Close>
|
|
</div>
|
|
|
|
{/* Chat */}
|
|
{provider === 'claude' ? (
|
|
<ClaudeInner
|
|
key="claude"
|
|
defaultInput={defaultInput}
|
|
cwd={cwd}
|
|
initialModel={taskSettings.defaultProvider === 'claude' ? taskSettings.defaultModel : null}
|
|
onProviderChange={setProvider}
|
|
taskInfo={taskInfo}
|
|
/>
|
|
) : (
|
|
<OpenCodeInner
|
|
key="opencode"
|
|
defaultInput={defaultInput}
|
|
cwd={cwd}
|
|
initialModel={taskSettings.defaultProvider === 'opencode' ? taskSettings.defaultModel : null}
|
|
onProviderChange={setProvider}
|
|
taskInfo={taskInfo}
|
|
/>
|
|
)}
|
|
</DialogPrimitive.Content>
|
|
</DialogPortal>
|
|
</Dialog>
|
|
);
|
|
};
|