sync: update from private repo (6fcb0d0)
This commit is contained in:
@@ -5,6 +5,7 @@ import { CreateLocalTaskInput, fetchMyOrgs, Visibility, listBrowserSessionProfil
|
||||
import { AttachmentDropzone } from './AttachmentDropzone';
|
||||
import { ScheduleFields } from './ScheduleFields';
|
||||
import { usePieceList } from '../../hooks/usePieces';
|
||||
import { resolvePieceOptions } from '../../lib/splitPieces';
|
||||
import { useAuthState } from '../../App';
|
||||
|
||||
interface CreateTaskDialogProps {
|
||||
@@ -80,7 +81,8 @@ export function CreateTaskDialog({ onClose, onSubmit, initialPiece, initialBody,
|
||||
scheduledAt: '',
|
||||
});
|
||||
|
||||
const selectedPiece = (pieces ?? []).find(p => p.name === form.piece);
|
||||
const resolvedPieces = resolvePieceOptions(pieces ?? []);
|
||||
const selectedPiece = resolvedPieces.find(p => p.name === form.piece);
|
||||
const missingMcp = selectedPiece?.requiredMcp
|
||||
? selectedPiece.requiredMcp.filter(
|
||||
(id) => !(connections ?? []).find((c) => c.serverId === id && c.connected),
|
||||
@@ -241,7 +243,7 @@ export function CreateTaskDialog({ onClose, onSubmit, initialPiece, initialBody,
|
||||
className="w-full px-2.5 py-1.5 border border-slate-200 rounded-lg text-xs outline-none focus:border-accent"
|
||||
>
|
||||
<option value="auto">自動選択</option>
|
||||
{(pieces ?? []).map(p => (
|
||||
{resolvedPieces.map(p => (
|
||||
<option key={p.name} value={p.name}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { continueTaskWithPiece, fetchLocalTaskComments } from '../../api';
|
||||
import { usePieceList } from '../../hooks/usePieces';
|
||||
import { resolvePieceOptions } from '../../lib/splitPieces';
|
||||
import { MarkdownText } from '../../lib/markdown-text';
|
||||
|
||||
interface PrevJobInfo {
|
||||
@@ -123,7 +124,7 @@ export function ContinueWithPieceDialog({
|
||||
disabled={piecesQuery.isLoading}
|
||||
className="px-3 py-2 rounded-md border border-hairline text-[13px] focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
|
||||
>
|
||||
{(piecesQuery.data ?? []).map(p => (
|
||||
{resolvePieceOptions(piecesQuery.data ?? []).map(p => (
|
||||
<option key={p.name} value={p.name}>
|
||||
{p.name}
|
||||
{p.name === prevJob.pieceName ? ' (現在)' : ''}
|
||||
|
||||
@@ -18,7 +18,7 @@ interface TopBarProps {
|
||||
export const NAV_ITEMS: Array<{ id: PageId; label: string; adminOnly: boolean; requiresAuth: boolean }> = [
|
||||
{ id: 'tasks', label: 'タスク', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'schedules', label: 'スケジュール', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'pieces', label: 'Pieces', adminOnly: true, requiresAuth: false },
|
||||
{ id: 'pieces', label: 'Pieces', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'captcha', label: 'CAPTCHA', adminOnly: true, requiresAuth: false },
|
||||
{ id: 'settings', label: '設定', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'users', label: 'ユーザー', adminOnly: true, requiresAuth: true },
|
||||
|
||||
@@ -7,9 +7,10 @@ export interface MovementAccordionProps {
|
||||
onAdd: () => void;
|
||||
onRemove: (index: number) => void;
|
||||
onMove: (index: number, direction: 'up' | 'down') => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove }: MovementAccordionProps) {
|
||||
export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove, disabled = false }: MovementAccordionProps) {
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||
const movementNames = movements.map((m) => m.name ?? '');
|
||||
|
||||
@@ -49,40 +50,42 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(i, 'up')}
|
||||
disabled={i === 0}
|
||||
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
|
||||
title="Move up"
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(i, 'down')}
|
||||
disabled={i === movements.length - 1}
|
||||
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
|
||||
title="Move down"
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm(`Movement "${movement.name}" を削除しますか?`)) {
|
||||
onRemove(i);
|
||||
if (expandedIndex === i) setExpandedIndex(null);
|
||||
}
|
||||
}}
|
||||
className="text-slate-400 hover:text-red-500 text-sm px-1"
|
||||
title="Delete"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{/* Controls — hidden in read-only mode */}
|
||||
{!disabled && (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(i, 'up')}
|
||||
disabled={i === 0}
|
||||
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
|
||||
title="Move up"
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(i, 'down')}
|
||||
disabled={i === movements.length - 1}
|
||||
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
|
||||
title="Move down"
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm(`Movement "${movement.name}" を削除しますか?`)) {
|
||||
onRemove(i);
|
||||
if (expandedIndex === i) setExpandedIndex(null);
|
||||
}
|
||||
}}
|
||||
className="text-slate-400 hover:text-red-500 text-sm px-1"
|
||||
title="Delete"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded form */}
|
||||
@@ -92,6 +95,7 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
|
||||
movement={movement}
|
||||
movementNames={movementNames}
|
||||
onChange={(field, value) => onChange(i, field, value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -100,13 +104,15 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
className="mt-3 text-sm text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
+ Add Movement
|
||||
</button>
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
className="mt-3 text-sm text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
+ Add Movement
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ export interface MovementFormProps {
|
||||
movement: any;
|
||||
movementNames: string[];
|
||||
onChange: (field: string, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function MovementForm({ movement, movementNames, onChange }: MovementFormProps) {
|
||||
export function MovementForm({ movement, movementNames, onChange, disabled = false }: MovementFormProps) {
|
||||
const nextOptions = [...movementNames.filter((n) => n !== movement.name), ...SPECIAL_TARGETS];
|
||||
const disabledClass = disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : '';
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -22,7 +24,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
type="text"
|
||||
value={movement.name ?? ''}
|
||||
onChange={(e) => onChange('name', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +36,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
type="text"
|
||||
value={movement.persona ?? ''}
|
||||
onChange={(e) => onChange('persona', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +47,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
<select
|
||||
value={movement.default_next ?? 'COMPLETE'}
|
||||
onChange={(e) => onChange('default_next', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`}
|
||||
>
|
||||
{nextOptions.map((opt) => (
|
||||
<option key={opt} value={opt}>{opt}</option>
|
||||
@@ -58,7 +63,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
id={`edit-${movement.name}`}
|
||||
checked={movement.edit ?? false}
|
||||
onChange={(e) => onChange('edit', e.target.checked)}
|
||||
className="rounded border-slate-300"
|
||||
disabled={disabled}
|
||||
className="rounded border-slate-300 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<label htmlFor={`edit-${movement.name}`} className="text-xs font-medium text-slate-600">edit</label>
|
||||
<HelpText>有効にすると Write / Edit ツールが LLM に提示されます</HelpText>
|
||||
@@ -71,7 +77,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
value={movement.instruction ?? ''}
|
||||
onChange={(e) => onChange('instruction', e.target.value)}
|
||||
rows={6}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono ${disabledClass}`}
|
||||
/>
|
||||
<HelpText>LLM に渡される指示文。Markdown 記法が使えます</HelpText>
|
||||
</div>
|
||||
@@ -80,6 +87,7 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
<ToolTagInput
|
||||
value={movement.allowed_tools ?? []}
|
||||
onChange={(tools) => onChange('allowed_tools', tools)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* rules */}
|
||||
@@ -87,6 +95,7 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
|
||||
rules={movement.rules ?? []}
|
||||
movementNames={movementNames.filter((n) => n !== movement.name)}
|
||||
onChange={(rules) => onChange('rules', rules)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,10 +9,22 @@ import { MovementAccordion } from './MovementAccordion';
|
||||
|
||||
export interface PieceEditorProps {
|
||||
name: string;
|
||||
/** Whether the current user has admin role. Controls edit access for built-in/global-custom pieces. */
|
||||
isAdmin?: boolean;
|
||||
/**
|
||||
* Source hint from URL / list selection — used for the targeted GET request.
|
||||
* The read-only gate uses the SOURCE the API actually resolved (authoritative),
|
||||
* so a deep link without pieceSource still renders correctly for non-admins.
|
||||
*/
|
||||
source?: 'builtin' | 'user-custom' | 'global-custom';
|
||||
}
|
||||
|
||||
export function PieceEditor({ name }: PieceEditorProps) {
|
||||
const { data: piece, isLoading, error } = usePiece(name);
|
||||
export function PieceEditor({ name, isAdmin = true, source }: PieceEditorProps) {
|
||||
const { data: fetchResult, isLoading, error } = usePiece(name, source);
|
||||
// Use the server-resolved source as the authoritative value; fall back to the
|
||||
// prop only while the fetch hasn't completed yet (avoids flicker on known paths).
|
||||
const piece = fetchResult?.piece ?? null;
|
||||
const effectiveSource = fetchResult?.source ?? source;
|
||||
const queryClient = useQueryClient();
|
||||
const { setUrlState } = useUrlState();
|
||||
|
||||
@@ -33,7 +45,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
setEditMode('visual');
|
||||
setYamlError(null);
|
||||
}
|
||||
}, [piece]);
|
||||
}, [piece]); // piece is derived from fetchResult above
|
||||
|
||||
const showToast = (msg: string, duration = 2000) => {
|
||||
setToast(msg);
|
||||
@@ -159,7 +171,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await updatePiece(name, saveData);
|
||||
await updatePiece(name, saveData, effectiveSource);
|
||||
await queryClient.invalidateQueries({ queryKey: ['piece', name] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['pieces'] });
|
||||
setIsDirty(false);
|
||||
@@ -177,7 +189,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
const handleDelete = async () => {
|
||||
if (!confirm(`Piece "${name}" を削除しますか?この操作は取り消せません。`)) return;
|
||||
try {
|
||||
await deletePiece(name);
|
||||
await deletePiece(name, effectiveSource);
|
||||
await queryClient.invalidateQueries({ queryKey: ['pieces'] });
|
||||
setUrlState((prev) => ({ ...prev, piece: undefined, section: 'provider' as any }));
|
||||
} catch (e: any) {
|
||||
@@ -189,6 +201,12 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
if (error) return <div className="text-sm text-red-500">Piece の読み込みに失敗しました</div>;
|
||||
if (!draft) return null;
|
||||
|
||||
// Non-admins cannot edit built-in or global-custom pieces — read-only view.
|
||||
// user-custom stays editable for its owner. Admins can edit all sources.
|
||||
// Use the SERVER-RESOLVED source (effectiveSource) as the authoritative value so
|
||||
// that deep links without a ?pieceSource param still render read-only for non-admins.
|
||||
const readonly = !isAdmin && (effectiveSource === 'builtin' || effectiveSource === 'global-custom');
|
||||
|
||||
const movementNames = (draft.movements ?? []).map((m: any) => m.name ?? '');
|
||||
|
||||
return (
|
||||
@@ -201,13 +219,21 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
<p className="text-sm text-slate-500 mt-0.5 line-clamp-2">{String(draft.description).split('\n')[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 rounded-lg border border-red-200"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{/* Delete is hidden for ALL built-in pieces (non-deletable by everyone, including admins). */}
|
||||
{!readonly && effectiveSource !== 'builtin' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 rounded-lg border border-red-200"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
{readonly && (
|
||||
<span className="px-2 py-1 text-xs text-slate-400 bg-slate-100 rounded border border-slate-200">
|
||||
読み取り専用
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mode toggle */}
|
||||
@@ -242,6 +268,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
piece={draft}
|
||||
onChange={handleMetaChange}
|
||||
movementNames={movementNames}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -253,6 +280,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
onAdd={handleAddMovement}
|
||||
onRemove={handleRemoveMovement}
|
||||
onMove={handleMoveMovement}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -268,7 +296,9 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
value={yamlText}
|
||||
onChange={(e) => handleYamlChange(e.target.value)}
|
||||
spellCheck={false}
|
||||
className="w-full px-4 py-3 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono bg-slate-50 leading-relaxed resize-y"
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
className={`w-full px-4 py-3 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono leading-relaxed resize-y ${readonly ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : 'bg-slate-50'}`}
|
||||
style={{ minHeight: '500px', tabSize: 2 }}
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
@@ -278,27 +308,29 @@ export function PieceEditor({ name }: PieceEditorProps) {
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 pt-4 mt-6 border-t border-slate-200">
|
||||
{toast && (
|
||||
<span className={`text-xs mr-auto ${toast.startsWith('エラー') ? 'text-red-500' : 'text-green-600'}`}>
|
||||
{toast}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleDiscard}
|
||||
disabled={!isDirty}
|
||||
className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Discard Changes
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty || saving}
|
||||
className="px-4 py-2 text-sm bg-accent text-accent-fg rounded-lg hover:bg-accent-deep disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className="flex items-center justify-end gap-3 pt-4 mt-6 border-t border-slate-200">
|
||||
{toast && (
|
||||
<span className={`text-xs mr-auto ${toast.startsWith('エラー') ? 'text-red-500' : 'text-green-600'}`}>
|
||||
{toast}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleDiscard}
|
||||
disabled={!isDirty}
|
||||
className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Discard Changes
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty || saving}
|
||||
className="px-4 py-2 text-sm bg-accent text-accent-fg rounded-lg hover:bg-accent-deep disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ export interface PieceMetaFormProps {
|
||||
piece: any;
|
||||
onChange: (field: string, value: any) => void;
|
||||
movementNames: string[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormProps) {
|
||||
export function PieceMetaForm({ piece, onChange, movementNames, disabled = false }: PieceMetaFormProps) {
|
||||
const triggersText = (piece.triggers?.keywords ?? []).join(', ');
|
||||
const disabledClass = disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : '';
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -30,7 +32,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
|
||||
type="text"
|
||||
value={piece.description ?? ''}
|
||||
onChange={(e) => onChange('description', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +45,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
|
||||
value={piece.max_movements ?? 10}
|
||||
onChange={(e) => onChange('max_movements', parseInt(e.target.value, 10) || 0)}
|
||||
min={1}
|
||||
className="w-32 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
|
||||
disabled={disabled}
|
||||
className={`w-32 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
|
||||
/>
|
||||
<HelpText>1 ジョブで実行できる movement の最大回数。ループ防止のため</HelpText>
|
||||
</div>
|
||||
@@ -53,7 +57,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
|
||||
<select
|
||||
value={piece.initial_movement ?? ''}
|
||||
onChange={(e) => onChange('initial_movement', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`}
|
||||
>
|
||||
{movementNames.length === 0 && <option value="">--</option>}
|
||||
{movementNames.map((name) => (
|
||||
@@ -77,7 +82,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
|
||||
onChange('triggers', { ...piece.triggers, keywords });
|
||||
}}
|
||||
placeholder="keyword1, keyword2, ..."
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
|
||||
/>
|
||||
<HelpText>タスク本文にこれらのキーワードが含まれると、この piece が自動選択されます</HelpText>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,12 @@ export interface RulesTableProps {
|
||||
rules: Array<{ condition: string; next: string }>;
|
||||
movementNames: string[];
|
||||
onChange: (rules: Array<{ condition: string; next: string }>) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function RulesTable({ rules, movementNames, onChange }: RulesTableProps) {
|
||||
export function RulesTable({ rules, movementNames, onChange, disabled = false }: RulesTableProps) {
|
||||
const nextOptions = [...movementNames, ...SPECIAL_TARGETS];
|
||||
const disabledClass = disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : '';
|
||||
|
||||
const updateRule = (index: number, field: 'condition' | 'next', value: string) => {
|
||||
const updated = rules.map((r, i) => (i === index ? { ...r, [field]: value } : r));
|
||||
@@ -33,7 +35,7 @@ export function RulesTable({ rules, movementNames, onChange }: RulesTableProps)
|
||||
<tr className="text-xs text-slate-500">
|
||||
<th className="text-left font-medium pb-1 pr-2">condition</th>
|
||||
<th className="text-left font-medium pb-1 pr-2 w-44">next</th>
|
||||
<th className="w-8" />
|
||||
{!disabled && <th className="w-8" />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -44,7 +46,8 @@ export function RulesTable({ rules, movementNames, onChange }: RulesTableProps)
|
||||
type="text"
|
||||
value={rule.condition}
|
||||
onChange={(e) => updateRule(i, 'condition', e.target.value)}
|
||||
className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
|
||||
placeholder="条件..."
|
||||
/>
|
||||
</td>
|
||||
@@ -52,34 +55,39 @@ export function RulesTable({ rules, movementNames, onChange }: RulesTableProps)
|
||||
<select
|
||||
value={rule.next}
|
||||
onChange={(e) => updateRule(i, 'next', e.target.value)}
|
||||
className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white"
|
||||
disabled={disabled}
|
||||
className={`w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`}
|
||||
>
|
||||
{nextOptions.map((opt) => (
|
||||
<option key={opt} value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="pb-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRule(i)}
|
||||
className="text-slate-400 hover:text-red-500 text-sm px-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
{!disabled && (
|
||||
<td className="pb-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRule(i)}
|
||||
className="text-slate-400 hover:text-red-500 text-sm px-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addRule}
|
||||
className="text-xs text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
+ Add Rule
|
||||
</button>
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={addRule}
|
||||
className="text-xs text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
+ Add Rule
|
||||
</button>
|
||||
)}
|
||||
<HelpText>LLM が transition ツールで遷移先を選ぶ際の条件です</HelpText>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { HelpText } from './HelpText';
|
||||
export interface ToolTagInputProps {
|
||||
value: string[];
|
||||
onChange: (tools: string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ export interface ToolTagInputProps {
|
||||
* - Selecting an unavailable catalog tool is still allowed (the user might be
|
||||
* preparing for a server that's about to come back online).
|
||||
*/
|
||||
export function ToolTagInput({ value, onChange }: ToolTagInputProps) {
|
||||
export function ToolTagInput({ value, onChange, disabled = false }: ToolTagInputProps) {
|
||||
const { data: catalog } = useToolList();
|
||||
const [input, setInput] = useState('');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
@@ -127,24 +128,27 @@ export function ToolTagInput({ value, onChange }: ToolTagInputProps) {
|
||||
name={tool}
|
||||
entry={entry}
|
||||
onRemove={() => removeTool(tool)}
|
||||
readOnly={disabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
setShowDropdown(true);
|
||||
}}
|
||||
onFocus={() => setShowDropdown(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={value.length === 0 ? 'ツール名を入力...' : ''}
|
||||
className="flex-1 min-w-[120px] text-sm outline-none bg-transparent"
|
||||
/>
|
||||
{!disabled && (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
setShowDropdown(true);
|
||||
}}
|
||||
onFocus={() => setShowDropdown(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={value.length === 0 ? 'ツール名を入力...' : ''}
|
||||
className="flex-1 min-w-[120px] text-sm outline-none bg-transparent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showDropdown && groupedSuggestions.length > 0 && (
|
||||
{!disabled && showDropdown && groupedSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full max-h-72 overflow-y-auto bg-white border border-slate-200 rounded-lg shadow-lg">
|
||||
{groupedSuggestions.map((g) => (
|
||||
<div key={g.key}>
|
||||
@@ -202,10 +206,12 @@ function SelectedToolChip({
|
||||
name,
|
||||
entry,
|
||||
onRemove,
|
||||
readOnly = false,
|
||||
}: {
|
||||
name: string;
|
||||
entry: ToolCatalogEntry | undefined;
|
||||
onRemove: () => void;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const isUnknown = !entry;
|
||||
const isUnavailable = entry ? !entry.available : false;
|
||||
@@ -231,14 +237,16 @@ function SelectedToolChip({
|
||||
{entry && <ScopeBadge scope={entry.scope} dim />}
|
||||
{isUnknown && <Badge color="amber">unknown</Badge>}
|
||||
{!isUnknown && isUnavailable && <Badge color="amber">{entry?.reason ?? 'offline'}</Badge>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-current opacity-60 hover:opacity-100"
|
||||
aria-label={`remove ${name}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-current opacity-60 hover:opacity-100"
|
||||
aria-label={`remove ${name}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user