Files
platform/src/workspaces/apps/FileBrowser/TaskRunnerModal.tsx
T

121 lines
4.4 KiB
TypeScript

import { 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 'apps/Chat';
import { usePi, EmbeddableChat } from 'apps/Chat';
import { useVisiblePiModels } from '@/state/useModels';
import { useSettings } from '@/state/useSettings';
import type { TaskSummary } from './useTasks';
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 PiMonoInnerProps = {
defaultInput: string;
cwd: { root?: string; path: string };
initialModel: string | null;
taskInfo: TaskInfo;
};
const PiMonoInner = ({
defaultInput,
cwd,
initialModel,
taskInfo,
}: PiMonoInnerProps) => {
const chat = usePi(undefined, initialModel, { replaceUrl: false, taskInfo });
const models = useVisiblePiModels();
const wasGenerating = useRef(false);
useEffect(() => {
if (wasGenerating.current && !chat.isGenerating) playDing();
wasGenerating.current = chat.isGenerating;
}, [chat.isGenerating]);
return (
<EmbeddableChat
chat={chat}
availableModels={models}
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 };
promptOverride?: string;
};
export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryType, cwd = { path: '' }, promptOverride }: TaskRunnerModalProps) => {
const { settings } = useSettings();
const taskSettings = settings.tasks;
const defaultInput = promptOverride
?? (entryName && entryType
? `Read the task instructions at ${task.filePath} and execute them on the ${entryType}: ${entryName}`
: `Read the task instructions at ${task.filePath} and execute them`);
const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName: entryName ?? '', entryType: entryType ?? 'file' };
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()}
>
{/* Header */}
<div className="shrink-0 flex items-center gap-3 px-5 py-3 border-b border-duck-dark/10 bg-background/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 */}
<PiMonoInner
key="pi"
defaultInput={defaultInput}
cwd={cwd}
initialModel={taskSettings.defaultProvider === 'pi' ? taskSettings.defaultModel : null}
taskInfo={taskInfo}
/>
</DialogPrimitive.Content>
</DialogPortal>
</Dialog>
);
};