import { useState, useEffect } from 'react'; import * as Dialog from '@radix-ui/react-dialog'; import { useQuery } from '@tanstack/react-query'; import { CreateLocalTaskInput, fetchMyOrgs, Visibility, listBrowserSessionProfiles } from '../../api'; import { AttachmentDropzone } from './AttachmentDropzone'; import { ScheduleFields } from './ScheduleFields'; import { usePieceList } from '../../hooks/usePieces'; import { useAuthState } from '../../App'; interface CreateTaskDialogProps { onClose: () => void; onSubmit: (input: CreateLocalTaskInput, attachments: Array<{ name: string; contentBase64: string }>) => Promise; /** * Optional preselected piece. When set, the piece is locked (not overridden * by the auto-classifier) and a placeholder hint explains the assistant. * Used by the Help Center "AI に聞く" button to land users in the help piece. */ initialPiece?: string; initialBody?: string; placeholder?: string; } export function CreateTaskDialog({ onClose, onSubmit, initialPiece, initialBody, placeholder }: CreateTaskDialogProps) { const { data: pieces } = usePieceList(); const { data: orgs = [] } = useQuery({ queryKey: ['my-orgs'], queryFn: fetchMyOrgs, staleTime: 5 * 60 * 1000 }); const { data: sessionProfiles = [] } = useQuery({ queryKey: ['browser-session-profiles'], queryFn: listBrowserSessionProfiles, staleTime: 60 * 1000, }); const activeSessionProfiles = sessionProfiles.filter(p => p.status === 'active'); interface ConnectionRow { serverId: string; serverName: string; connected: boolean } const { data: connections } = useQuery({ queryKey: ['mcp-connections'], queryFn: async (): Promise => { const res = await fetch('/api/mcp/connections', { credentials: 'include' }); if (!res.ok) return []; const data = await res.json(); return (data.connections ?? []) as ConnectionRow[]; }, staleTime: 30_000, }); const authState = useAuthState(); const defaultVis = (authState.mode === 'authenticated' ? authState.user?.defaultVisibility : undefined) ?? 'private'; const savedOrgId = (authState.mode === 'authenticated' ? authState.user?.defaultVisibilityOrgId : undefined) ?? null; const [visibility, setVisibility] = useState(defaultVis); const [visibilityScopeOrgId, setVisibilityScopeOrgId] = useState(savedOrgId); // Backfill the scope when orgs finish loading: useQuery starts with orgs=[] // so without this the initial render freezes scopeId=null for users without // a saved default, and picking 'Organization' would submit an unscoped task. useEffect(() => { if (visibilityScopeOrgId !== null) return; if (orgs.length === 0) return; setVisibilityScopeOrgId(orgs[0].orgId); }, [orgs, visibilityScopeOrgId]); const [form, setForm] = useState({ body: initialBody ?? '', piece: initialPiece ?? 'auto', profile: 'auto', outputFormat: 'markdown', askPolicy: 'low', priority: 'medium', }); const [attachments, setAttachments] = useState>([]); const [browserSessionProfileId, setBrowserSessionProfileId] = useState(null); const [mcpDisabled, setMcpDisabled] = useState(false); const [skillsDisabled, setSkillsDisabled] = useState(false); const [error, setError] = useState(''); const [submitting, setSubmitting] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [isScheduled, setIsScheduled] = useState(false); const [schedule, setSchedule] = useState({ scheduleType: 'daily', hour: 9, minute: 0, dayOfWeek: 1, dayOfMonth: 1, cronExpression: '', scheduledAt: '', }); const selectedPiece = (pieces ?? []).find(p => p.name === form.piece); const missingMcp = selectedPiece?.requiredMcp ? selectedPiece.requiredMcp.filter( (id) => !(connections ?? []).find((c) => c.serverId === id && c.connected), ) : []; const handleSubmit = async () => { if (!form.body.trim()) { setError('依頼内容は必須です'); return; } try { setSubmitting(true); setError(''); if (isScheduled) { const res = await fetch('/api/scheduled-tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: form.body.trim().slice(0, 40), body: form.body.trim(), piece: form.piece, visibility, visibilityScopeOrgId: visibility === 'org' ? visibilityScopeOrgId : null, browserSessionProfileId: browserSessionProfileId ?? undefined, ...schedule, }), }); if (!res.ok) throw new Error('スケジュール作成に失敗しました'); onClose(); return; } const options: Record = {}; if (mcpDisabled) options.mcpDisabled = true; if (skillsDisabled) options.skillsDisabled = true; const submitForm = { ...form, // initialPiece が指定されているヘルプアシスタント等は piece を固定。 // それ以外は form.piece (詳細設定で選択した値、無指定なら 'auto') を尊重する。 piece: initialPiece ?? form.piece, title: undefined, body: form.body.trim(), visibility, visibilityScopeOrgId: visibility === 'org' ? visibilityScopeOrgId : null, browserSessionProfileId: browserSessionProfileId ?? undefined, ...(Object.keys(options).length > 0 ? { options } : {}), }; await onSubmit(submitForm, attachments); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setSubmitting(false); } }; return ( { if (!open) onClose(); }}> { e.preventDefault(); }} >
{initialPiece === 'help' ? 'AI ヘルプに質問' : '新しい Task'} {initialPiece === 'help' ? '使い方や設計について自由に質問してください' : '依頼内容を入力して実行'}
{/* Textarea */}