Files
kandev/apps/web/components/quick-chat/quick-chat-dialog.tsx
T
2026-03-08 22:37:37 +00:00

170 lines
5.8 KiB
TypeScript

"use client";
import { memo, useCallback, useState } from "react";
import { Dialog, DialogContent, DialogTitle } from "@kandev/ui/dialog";
import { Button } from "@kandev/ui/button";
import { Label } from "@kandev/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { IconX, IconRocket } from "@tabler/icons-react";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
import { startQuickChat } from "@/lib/api/domains/workspace-api";
import type { Repository } from "@/lib/types/http";
import type { AgentProfileOption } from "@/lib/state/slices/settings/types";
type QuickChatPickerDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
workspaceId: string;
};
type FormState = {
selectedRepoId: string;
setSelectedRepoId: (id: string) => void;
selectedAgentId: string;
setSelectedAgentId: (id: string) => void;
repositories: Repository[];
agentProfiles: AgentProfileOption[];
};
const NONE_VALUE = "__none__";
function QuickChatFormBody({ state }: { state: FormState }) {
const { selectedRepoId, setSelectedRepoId, selectedAgentId, setSelectedAgentId } = state;
return (
<div className="p-4 space-y-4">
<p className="text-sm text-muted-foreground">
Start a quick conversation with an agent without creating a formal task.
</p>
<div className="space-y-2">
<Label htmlFor="repository">Repository (optional)</Label>
<Select
value={selectedRepoId || NONE_VALUE}
onValueChange={(v) => setSelectedRepoId(v === NONE_VALUE ? "" : v)}
>
<SelectTrigger id="repository" className="w-full">
<SelectValue placeholder="Select a repository..." />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_VALUE}>No repository</SelectItem>
{state.repositories.map((repo) => (
<SelectItem key={repo.id} value={repo.id}>
{repo.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="agent">Agent (optional)</Label>
<Select
value={selectedAgentId || NONE_VALUE}
onValueChange={(v) => setSelectedAgentId(v === NONE_VALUE ? "" : v)}
>
<SelectTrigger id="agent" className="w-full">
<SelectValue placeholder="Use workspace default..." />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_VALUE}>Use workspace default</SelectItem>
{state.agentProfiles.map((profile) => (
<SelectItem key={profile.id} value={profile.id}>
{profile.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
}
/** Dialog to pick repository and agent for a new quick chat session */
export const QuickChatPickerDialog = memo(function QuickChatPickerDialog({
open,
onOpenChange,
workspaceId,
}: QuickChatPickerDialogProps) {
const { toast } = useToast();
const openQuickChat = useAppStore((s) => s.openQuickChat);
const [isStarting, setIsStarting] = useState(false);
const [selectedRepoId, setSelectedRepoId] = useState<string>("");
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
const repositories = useAppStore((s) => s.repositories.itemsByWorkspaceId?.[workspaceId] ?? []);
const agentProfiles = useAppStore((s) => s.agentProfiles.items ?? []);
const handleStart = useCallback(async () => {
if (isStarting) return;
setIsStarting(true);
try {
const response = await startQuickChat(workspaceId, {
repository_id: selectedRepoId || undefined,
agent_profile_id: selectedAgentId || undefined,
});
onOpenChange(false);
// Open the quick chat modal with the new session
openQuickChat(response.session_id, workspaceId);
} catch (error) {
toast({
title: "Failed to start quick chat",
description: error instanceof Error ? error.message : "Unknown error",
variant: "error",
});
} finally {
setIsStarting(false);
}
}, [
workspaceId,
selectedRepoId,
selectedAgentId,
isStarting,
onOpenChange,
openQuickChat,
toast,
]);
const formState: FormState = {
selectedRepoId,
setSelectedRepoId,
selectedAgentId,
setSelectedAgentId,
repositories,
agentProfiles,
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="!max-w-md p-0 gap-0 flex flex-col shadow-2xl"
showCloseButton={false}
overlayClassName="bg-black/20"
>
<DialogTitle className="sr-only">New Quick Chat</DialogTitle>
<div className="flex items-center justify-between px-4 py-3 border-b">
<h2 className="text-lg font-semibold">New Quick Chat</h2>
<Button
variant="ghost"
size="icon"
onClick={() => onOpenChange(false)}
className="cursor-pointer h-8 w-8"
>
<IconX className="h-4 w-4" />
</Button>
</div>
<QuickChatFormBody state={formState} />
<div className="flex justify-end gap-2 px-4 py-3 border-t bg-muted/30">
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
Cancel
</Button>
<Button onClick={handleStart} disabled={isStarting} className="cursor-pointer">
<IconRocket className="h-4 w-4 mr-2" />
{isStarting ? "Starting..." : "Start Chat"}
</Button>
</div>
</DialogContent>
</Dialog>
);
});
/** Alias for backwards compatibility */
export const QuickChatDialog = QuickChatPickerDialog;