|
@@ -20,6 +20,7 @@ export default function EventGraphEditor({ projectPath, apiSettings }: EventGrap
|
|
|
const [aiFeedback, setAiFeedback] = useState<string | null>(null);
|
|
const [aiFeedback, setAiFeedback] = useState<string | null>(null);
|
|
|
const [isAllocating, setIsAllocating] = useState(false);
|
|
const [isAllocating, setIsAllocating] = useState(false);
|
|
|
const [isValidating, setIsValidating] = useState(false);
|
|
const [isValidating, setIsValidating] = useState(false);
|
|
|
|
|
+ const [newEdgeType, setNewEdgeType] = useState<EventEdge['relationType']>('causal');
|
|
|
|
|
|
|
|
// Load event graph
|
|
// Load event graph
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -78,6 +79,20 @@ export default function EventGraphEditor({ projectPath, apiSettings }: EventGrap
|
|
|
if (selectedEvent?.id === id) setSelectedEvent(null);
|
|
if (selectedEvent?.id === id) setSelectedEvent(null);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // Add edge
|
|
|
|
|
+ const handleAddEdge = (targetId: string) => {
|
|
|
|
|
+ if (!targetId || !selectedEvent) return;
|
|
|
|
|
+ const newEdge: EventEdge = {
|
|
|
|
|
+ sourceId: selectedEvent.id,
|
|
|
|
|
+ targetId,
|
|
|
|
|
+ relationType: newEdgeType,
|
|
|
|
|
+ };
|
|
|
|
|
+ if (edges.some(ex => ex.sourceId === newEdge.sourceId && ex.targetId === newEdge.targetId)) return;
|
|
|
|
|
+ const updated = [...edges, newEdge];
|
|
|
|
|
+ setEdges(updated);
|
|
|
|
|
+ saveEdges(updated);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// Narrative reorder
|
|
// Narrative reorder
|
|
|
const moveNarrativeUp = (eventId: string) => {
|
|
const moveNarrativeUp = (eventId: string) => {
|
|
|
const sorted = [...events].sort((a, b) => a.narrativeOrder - b.narrativeOrder);
|
|
const sorted = [...events].sort((a, b) => a.narrativeOrder - b.narrativeOrder);
|
|
@@ -227,6 +242,16 @@ export default function EventGraphEditor({ projectPath, apiSettings }: EventGrap
|
|
|
>
|
|
>
|
|
|
<span className="font-medium">{event.title}</span>
|
|
<span className="font-medium">{event.title}</span>
|
|
|
<span className="text-gray-400 ml-2">{event.timeLabel}</span>
|
|
<span className="text-gray-400 ml-2">{event.timeLabel}</span>
|
|
|
|
|
+ {(() => {
|
|
|
|
|
+ const hasOutgoing = edges.some(e => e.sourceId === event.id);
|
|
|
|
|
+ const hasIncoming = edges.some(e => e.targetId === event.id);
|
|
|
|
|
+ return (hasOutgoing || hasIncoming) ? (
|
|
|
|
|
+ <span className="flex gap-0.5 ml-1">
|
|
|
|
|
+ {hasOutgoing && <span className="w-1 h-1 rounded-full bg-red-400" title="有出边" />}
|
|
|
|
|
+ {hasIncoming && <span className="w-1 h-1 rounded-full bg-blue-400" title="有入边" />}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : null;
|
|
|
|
|
+ })()}
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
@@ -252,6 +277,16 @@ export default function EventGraphEditor({ projectPath, apiSettings }: EventGrap
|
|
|
</div>
|
|
</div>
|
|
|
<GripVertical className="h-3 w-3 text-gray-300 shrink-0" />
|
|
<GripVertical className="h-3 w-3 text-gray-300 shrink-0" />
|
|
|
<span className="font-medium">{event.title}</span>
|
|
<span className="font-medium">{event.title}</span>
|
|
|
|
|
+ {(() => {
|
|
|
|
|
+ const hasOutgoing = edges.some(e => e.sourceId === event.id);
|
|
|
|
|
+ const hasIncoming = edges.some(e => e.targetId === event.id);
|
|
|
|
|
+ return (hasOutgoing || hasIncoming) ? (
|
|
|
|
|
+ <span className="flex gap-0.5 ml-1">
|
|
|
|
|
+ {hasOutgoing && <span className="w-1 h-1 rounded-full bg-red-400" title="有出边" />}
|
|
|
|
|
+ {hasIncoming && <span className="w-1 h-1 rounded-full bg-blue-400" title="有入边" />}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : null;
|
|
|
|
|
+ })()}
|
|
|
{event.eventType === 'flashback' && <span className="text-blue-500 ml-1">↓倒叙</span>}
|
|
{event.eventType === 'flashback' && <span className="text-blue-500 ml-1">↓倒叙</span>}
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
@@ -316,6 +351,58 @@ export default function EventGraphEditor({ projectPath, apiSettings }: EventGrap
|
|
|
: <span className="text-gray-400">未分配</span>;
|
|
: <span className="text-gray-400">未分配</span>;
|
|
|
})()}
|
|
})()}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Edge management */}
|
|
|
|
|
+ <div className="mt-4 pt-3 border-t border-gray-200">
|
|
|
|
|
+ <label className="text-xs text-gray-500">关联事件</label>
|
|
|
|
|
+ <div className="space-y-1 mt-1">
|
|
|
|
|
+ {edges
|
|
|
|
|
+ .filter(e => e.sourceId === selectedEvent.id || e.targetId === selectedEvent.id)
|
|
|
|
|
+ .map((edge, i) => {
|
|
|
|
|
+ const otherId = edge.sourceId === selectedEvent.id ? edge.targetId : edge.sourceId;
|
|
|
|
|
+ const otherEvent = events.find(e => e.id === otherId);
|
|
|
|
|
+ const direction = edge.sourceId === selectedEvent.id ? '→' : '←';
|
|
|
|
|
+ const relLabel = edge.relationType === 'causal' ? '因果' : edge.relationType === 'temporal' ? '时序' : edge.relationType === 'parallel' ? '并列' : '对比';
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={i} className="flex items-center gap-1 text-xs text-gray-600">
|
|
|
|
|
+ <span className="text-gray-400">{direction}</span>
|
|
|
|
|
+ <span>{otherEvent?.title || otherId.slice(0, 8)}</span>
|
|
|
|
|
+ <span className="text-gray-400">({relLabel})</span>
|
|
|
|
|
+ <button onClick={() => {
|
|
|
|
|
+ const updated = edges.filter(e => !(e.sourceId === edge.sourceId && e.targetId === edge.targetId));
|
|
|
|
|
+ setEdges(updated);
|
|
|
|
|
+ saveEdges(updated);
|
|
|
|
|
+ }} className="ml-auto text-red-400 hover:text-red-600 text-xs">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex gap-1 mt-2">
|
|
|
|
|
+ <select
|
|
|
|
|
+ className="flex-1 px-1 py-1 text-xs border border-gray-200 rounded"
|
|
|
|
|
+ onChange={(e) => {
|
|
|
|
|
+ handleAddEdge(e.target.value);
|
|
|
|
|
+ e.target.value = '';
|
|
|
|
|
+ }}
|
|
|
|
|
+ defaultValue=""
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="" disabled>添加关联...</option>
|
|
|
|
|
+ {events.filter(e => e.id !== selectedEvent.id).map(e => (
|
|
|
|
|
+ <option key={e.id} value={e.id}>{e.title}</option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ <select
|
|
|
|
|
+ className="w-16 px-1 py-1 text-xs border border-gray-200 rounded"
|
|
|
|
|
+ value={newEdgeType}
|
|
|
|
|
+ onChange={(e) => setNewEdgeType(e.target.value as EventEdge['relationType'])}
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="causal">因果</option>
|
|
|
|
|
+ <option value="temporal">时序</option>
|
|
|
|
|
+ <option value="parallel">并列</option>
|
|
|
|
|
+ <option value="contrast">对比</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
) : (
|
|
) : (
|
|
|
<div className="text-center text-gray-400 text-sm mt-12">选择一个事件查看和编辑详情</div>
|
|
<div className="text-center text-gray-400 text-sm mt-12">选择一个事件查看和编辑详情</div>
|