feat: AI marker system prompt fixes, TaskLane activation, and FlowPilot updates
- Fix system prompt to ensure [QUESTIONS]/[ACTIONS] markers in AI responses - Add format reminder injection to user messages for marker compliance - Wire TaskLane activation in prefill and resume paths - Add ActionCardGroup component for structured question/action rendering - Update FlowPilot session and step card components - Update ai-session schemas and types for marker data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
300
frontend/src/components/assistant/ActionCardGroup.tsx
Normal file
300
frontend/src/components/assistant/ActionCardGroup.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import { useState } from 'react'
|
||||
import { Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp, Send, Clipboard, Loader2, AlertCircle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { ActionItem } from '@/types/ai-session'
|
||||
|
||||
type CardState = 'pending' | 'pasting' | 'typing' | 'skipped' | 'done'
|
||||
|
||||
interface CardResponse {
|
||||
label: string
|
||||
state: CardState
|
||||
value: string
|
||||
}
|
||||
|
||||
interface ActionCardGroupProps {
|
||||
actions: ActionItem[]
|
||||
onSubmit: (responses: CardResponse[]) => void
|
||||
disabled?: boolean
|
||||
stale?: boolean
|
||||
}
|
||||
|
||||
export function ActionCardGroup({ actions, onSubmit, disabled, stale }: ActionCardGroupProps) {
|
||||
const [responses, setResponses] = useState<CardResponse[]>(
|
||||
actions.map(a => ({ label: a.label, state: 'pending', value: '' }))
|
||||
)
|
||||
const [showRunAll, setShowRunAll] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [submitError, setSubmitError] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const anyPending = responses.some(r => r.state === 'pending')
|
||||
const isCollapsed = stale && anyPending && !expanded
|
||||
|
||||
const updateCard = (idx: number, updates: Partial<CardResponse>) => {
|
||||
setResponses(prev => prev.map((r, i) => i === idx ? { ...r, ...updates } : r))
|
||||
}
|
||||
|
||||
const allHandled = responses.every(r => r.state !== 'pending' && r.state !== 'pasting' && r.state !== 'typing')
|
||||
const anyInteracted = responses.some(r => r.state !== 'pending')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitting(true)
|
||||
setSubmitError(false)
|
||||
try {
|
||||
onSubmit(responses)
|
||||
setSubmitted(true)
|
||||
} catch {
|
||||
setSubmitError(true)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyCommand = (command: string) => {
|
||||
navigator.clipboard.writeText(command)
|
||||
toast.success('Copied to clipboard')
|
||||
}
|
||||
|
||||
// Build combined script for "Run All"
|
||||
const commandActions = actions.filter(a => a.command)
|
||||
const combinedScript = commandActions.map((a, i) => (
|
||||
`# ── ${i + 1}. ${a.label} ──\n${a.command}`
|
||||
)).join('\n\n')
|
||||
|
||||
const doneCount = responses.filter(r => r.state === 'done').length
|
||||
const skippedCount = responses.filter(r => r.state === 'skipped').length
|
||||
|
||||
// ── Collapsed state (stale cards from earlier in conversation) ──
|
||||
if (isCollapsed) {
|
||||
const pendingCount = responses.filter(r => r.state === 'pending').length
|
||||
return (
|
||||
<button
|
||||
onClick={() => setExpanded(true)}
|
||||
className="w-full rounded-lg border border-default/50 bg-elevated/20 p-2.5 flex items-center justify-between text-left hover:bg-elevated/40 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-[0.75rem] text-muted-foreground">
|
||||
<Terminal size={12} />
|
||||
<span>{pendingCount} diagnostic check{pendingCount !== 1 ? 's' : ''} — not completed</span>
|
||||
</div>
|
||||
<span className="text-[0.6875rem] text-accent-text opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Expand
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Submitted state ──
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="rounded-lg border border-success/20 bg-success-dim/20 p-3 space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-[0.8125rem] font-medium text-success">
|
||||
<Check size={14} />
|
||||
<span>{doneCount} checked, {skippedCount} skipped</span>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{responses.map((r, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[0.75rem] text-muted-foreground">
|
||||
{r.state === 'done' ? (
|
||||
<Check size={10} className="text-success shrink-0" />
|
||||
) : (
|
||||
<SkipForward size={10} className="text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<span className={r.state === 'skipped' ? 'line-through opacity-60' : ''}>
|
||||
{r.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Run All button — only if multiple commands exist */}
|
||||
{commandActions.length > 1 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowRunAll(!showRunAll)}
|
||||
className="flex items-center gap-1.5 text-[0.75rem] font-medium text-accent-text hover:text-accent transition-colors"
|
||||
>
|
||||
<Terminal size={12} />
|
||||
<span>Run All ({commandActions.length} commands)</span>
|
||||
{showRunAll ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</button>
|
||||
|
||||
{showRunAll && (
|
||||
<div className="mt-2 rounded-lg border border-default bg-code p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Combined diagnostic script
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleCopyCommand(combinedScript)}
|
||||
className="flex items-center gap-1 text-[0.75rem] text-muted-foreground hover:text-heading transition-colors"
|
||||
>
|
||||
<Copy size={11} />
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre className="text-[0.8125rem] font-mono text-heading whitespace-pre-wrap overflow-x-auto">
|
||||
{combinedScript}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Individual action cards */}
|
||||
{actions.map((action, idx) => {
|
||||
const response = responses[idx]
|
||||
const isExpanded = response.state === 'pasting' || response.state === 'typing'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn(
|
||||
'rounded-lg border p-3 transition-all',
|
||||
response.state === 'done' ? 'border-success/30 bg-success-dim/30' :
|
||||
response.state === 'skipped' ? 'border-default/50 bg-elevated/20 opacity-60' :
|
||||
'border-default bg-card hover:border-hover'
|
||||
)}
|
||||
>
|
||||
{/* Card header */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[0.8125rem] font-medium text-heading">{action.label}</div>
|
||||
{action.description && (
|
||||
<div className="text-[0.75rem] text-muted-foreground mt-0.5">{action.description}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status badge for handled cards */}
|
||||
{response.state === 'done' && (
|
||||
<span className="shrink-0 text-[10px] font-semibold uppercase tracking-wider text-success">Done</span>
|
||||
)}
|
||||
{response.state === 'skipped' && (
|
||||
<span className="shrink-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Skipped</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Command with copy button */}
|
||||
{action.command && response.state !== 'skipped' && (
|
||||
<div className="mt-2 flex items-center gap-2 rounded bg-code px-2.5 py-1.5">
|
||||
<code className="flex-1 text-[0.75rem] font-mono text-heading truncate">
|
||||
{action.command}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => handleCopyCommand(action.command!)}
|
||||
className="shrink-0 text-muted-foreground hover:text-heading transition-colors"
|
||||
title="Copy command"
|
||||
>
|
||||
<Copy size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons — only for pending cards */}
|
||||
{response.state === 'pending' && !disabled && (
|
||||
<div className="mt-2 flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<button
|
||||
onClick={() => updateCard(idx, { state: 'pasting' })}
|
||||
className="flex items-center justify-center gap-1 rounded-md border border-accent/40 bg-accent-dim/30 px-2.5 py-2 sm:py-1 text-[0.75rem] font-medium text-accent-text hover:bg-accent-dim/50 transition-colors min-h-[44px] sm:min-h-0"
|
||||
>
|
||||
<Clipboard size={11} />
|
||||
Paste Result
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateCard(idx, { state: 'typing' })}
|
||||
className="flex items-center justify-center gap-1 rounded-md border border-default bg-elevated/50 px-2.5 py-2 sm:py-1 text-[0.75rem] font-medium text-heading hover:bg-elevated transition-colors min-h-[44px] sm:min-h-0"
|
||||
>
|
||||
Type Answer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateCard(idx, { state: 'skipped' })}
|
||||
className="flex items-center justify-center gap-1 rounded-md px-2.5 py-2 sm:py-1 text-[0.75rem] text-muted-foreground hover:text-heading transition-colors min-h-[44px] sm:min-h-0"
|
||||
>
|
||||
<SkipForward size={11} />
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded input area */}
|
||||
{isExpanded && (
|
||||
<div className="mt-2">
|
||||
<textarea
|
||||
autoFocus
|
||||
value={response.value}
|
||||
onChange={e => updateCard(idx, { value: e.target.value })}
|
||||
placeholder={response.state === 'pasting' ? 'Paste command output here...' : 'Type your answer...'}
|
||||
className="w-full rounded-md border border-default bg-input px-3 py-2 text-[0.8125rem] text-heading placeholder:text-muted-foreground font-mono resize-y min-h-[60px] max-h-[200px] overflow-y-auto focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => updateCard(idx, { state: 'done' })}
|
||||
disabled={!response.value.trim()}
|
||||
className="flex items-center gap-1 rounded-md bg-accent px-2.5 py-1 text-[0.75rem] font-medium text-white disabled:opacity-40 hover:bg-accent-hover transition-colors"
|
||||
>
|
||||
<Check size={11} />
|
||||
Done
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateCard(idx, { state: 'pending', value: '' })}
|
||||
className="text-[0.75rem] text-muted-foreground hover:text-heading transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Submit / Error / Loading */}
|
||||
{anyInteracted && (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!allHandled || disabled || submitting}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-lg px-4 py-2 text-[0.8125rem] font-medium transition-colors',
|
||||
allHandled && !submitting
|
||||
? 'bg-accent text-white hover:bg-accent-hover'
|
||||
: 'bg-elevated text-muted-foreground cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send size={13} />
|
||||
Send Responses
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{submitError && (
|
||||
<div className="flex items-center gap-1.5 text-[0.75rem] text-danger">
|
||||
<AlertCircle size={12} />
|
||||
<span>Failed to send</span>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="underline hover:no-underline"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
StatusUpdateContext,
|
||||
StatusUpdateResponse,
|
||||
} from '@/types/ai-session'
|
||||
import type { BranchResponse } from '@/types/branching'
|
||||
import { ConfidenceIndicator } from './ConfidenceIndicator'
|
||||
import { FlowPilotStepCard } from './FlowPilotStepCard'
|
||||
import { FlowPilotMessageBar } from './FlowPilotMessageBar'
|
||||
@@ -17,6 +18,8 @@ import { SessionDocView } from './SessionDocView'
|
||||
import { StatusUpdateModal } from './StatusUpdateModal'
|
||||
import { SessionTicketCard } from './SessionTicketCard'
|
||||
import { SimilarSessions } from './SimilarSessions'
|
||||
import { BranchMap } from '@/components/session/BranchMap'
|
||||
import { BranchTransitionBar } from '@/components/session/BranchTransitionBar'
|
||||
import { TicketPickerModal } from '@/components/session/TicketPickerModal'
|
||||
import { aiSessionsApi } from '@/api'
|
||||
import { toast } from '@/lib/toast'
|
||||
@@ -36,6 +39,10 @@ interface FlowPilotSessionProps {
|
||||
onRate: (rating: number) => void
|
||||
onReloadSession?: () => Promise<void>
|
||||
onGenerateStatusUpdate?: (audience: StatusUpdateAudience, length: StatusUpdateLength, context: StatusUpdateContext) => Promise<StatusUpdateResponse>
|
||||
// Branching props (optional — only present for branching sessions)
|
||||
branches?: BranchResponse[]
|
||||
activeBranchId?: string | null
|
||||
onBranchSwitch?: (branchId: string) => void
|
||||
}
|
||||
|
||||
export function FlowPilotSession({
|
||||
@@ -52,12 +59,34 @@ export function FlowPilotSession({
|
||||
onRate,
|
||||
onReloadSession,
|
||||
onGenerateStatusUpdate,
|
||||
branches,
|
||||
activeBranchId,
|
||||
onBranchSwitch,
|
||||
}: FlowPilotSessionProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [showTicketPicker, setShowTicketPicker] = useState(false)
|
||||
const [linkingTicket, setLinkingTicket] = useState(false)
|
||||
const [showShareCommunication, setShowShareCommunication] = useState(false)
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false)
|
||||
const prevBranchIdRef = useRef<string | null>(null)
|
||||
const [branchTransition, setBranchTransition] = useState<{
|
||||
from: BranchResponse | null
|
||||
to: BranchResponse
|
||||
} | null>(null)
|
||||
|
||||
// Track branch switches and show transition bar
|
||||
useEffect(() => {
|
||||
if (!activeBranchId || !branches?.length) return
|
||||
const prev = prevBranchIdRef.current
|
||||
if (prev && prev !== activeBranchId) {
|
||||
const fromBranch = branches.find(b => b.id === prev) ?? null
|
||||
const toBranch = branches.find(b => b.id === activeBranchId)
|
||||
if (toBranch) {
|
||||
setBranchTransition({ from: fromBranch, to: toBranch })
|
||||
}
|
||||
}
|
||||
prevBranchIdRef.current = activeBranchId
|
||||
}, [activeBranchId, branches])
|
||||
|
||||
const handleLinkTicket = async (ticketId: string, _ticket: PSATicketInfo) => {
|
||||
if (!session.psa_connection_id && !session.ticket_data) {
|
||||
@@ -218,6 +247,14 @@ export function FlowPilotSession({
|
||||
{/* Conversation column — pb-24 provides clearance for the fixed message bar */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3 pb-24 sm:p-4 sm:pb-24 lg:p-6 lg:pb-24">
|
||||
<div className="mx-auto max-w-2xl space-y-3">
|
||||
{/* Branch transition bar */}
|
||||
{branchTransition && (
|
||||
<BranchTransitionBar
|
||||
fromBranch={branchTransition.from}
|
||||
toBranch={branchTransition.to}
|
||||
/>
|
||||
)}
|
||||
|
||||
{allSteps.map((step) => (
|
||||
<FlowPilotStepCard
|
||||
key={step.step_id}
|
||||
@@ -226,6 +263,8 @@ export function FlowPilotSession({
|
||||
isProcessing={isProcessing && currentStep?.step_id === step.step_id}
|
||||
sessionId={session.id}
|
||||
onRespond={onRespond}
|
||||
onBranchSwitch={onBranchSwitch}
|
||||
activeBranchId={activeBranchId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -236,6 +275,15 @@ export function FlowPilotSession({
|
||||
className="hidden w-72 shrink-0 overflow-y-auto border-l border-border p-4 lg:block"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Branch map (branching sessions only) */}
|
||||
{session.is_branching && branches && branches.length > 0 && onBranchSwitch && (
|
||||
<BranchMap
|
||||
branches={branches}
|
||||
activeBranchId={activeBranchId ?? null}
|
||||
onSelectBranch={onBranchSwitch}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ticket context */}
|
||||
{session.psa_ticket_id ? (
|
||||
<SessionTicketCard
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { MessageSquare, Zap, CheckCircle2, SkipForward, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { MessageSquare, Zap, CheckCircle2, SkipForward, ChevronDown, ChevronUp, GitFork } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { AISessionStepResponse, StepResponseRequest } from '@/types/ai-session'
|
||||
import { isScriptGenerationAction, isScriptBuilderAction, getActionType } from '@/types/ai-session'
|
||||
@@ -13,6 +13,8 @@ interface FlowPilotStepCardProps {
|
||||
isProcessing: boolean
|
||||
sessionId?: string
|
||||
onRespond: (response: StepResponseRequest) => void
|
||||
onBranchSwitch?: (branchId: string) => void
|
||||
activeBranchId?: string | null
|
||||
}
|
||||
|
||||
const STEP_TYPE_ICONS = {
|
||||
@@ -23,9 +25,10 @@ const STEP_TYPE_ICONS = {
|
||||
info_request: MessageSquare,
|
||||
script_generation: Zap,
|
||||
note: MessageSquare,
|
||||
fork: GitFork,
|
||||
} as const
|
||||
|
||||
export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId, onRespond }: FlowPilotStepCardProps) {
|
||||
export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId, onRespond, onBranchSwitch, activeBranchId }: FlowPilotStepCardProps) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(!isCurrentStep)
|
||||
|
||||
const content = step.content as Record<string, unknown>
|
||||
@@ -94,6 +97,65 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
)
|
||||
}
|
||||
|
||||
// Fork step — special rendering with branch options
|
||||
if (contentType === 'fork') {
|
||||
const forkReason = (content.fork_reason as string) || stepText
|
||||
const forkBranches = (content.fork_branches as Array<{ branch_id: string; label: string }>) || []
|
||||
|
||||
return (
|
||||
<div className="card-flat p-3 sm:p-4 lg:p-5 border-accent/30">
|
||||
{/* Context message */}
|
||||
{step.context_message && (
|
||||
<div className="mb-3 rounded-lg bg-primary/5 px-3 py-2 border border-primary/10">
|
||||
<MarkdownContent content={step.context_message} className="text-xs text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fork header */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-accent-dim">
|
||||
<GitFork size={14} className="text-accent" />
|
||||
</span>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-accent-text">
|
||||
Diagnostic Fork
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Fork reason */}
|
||||
<MarkdownContent content={forkReason} className="text-sm mb-4" />
|
||||
|
||||
{/* Branch options */}
|
||||
{forkBranches.length > 0 && onBranchSwitch && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{forkBranches.map((branch) => {
|
||||
const isActive = branch.branch_id === activeBranchId
|
||||
return (
|
||||
<button
|
||||
key={branch.branch_id}
|
||||
onClick={() => onBranchSwitch(branch.branch_id)}
|
||||
className={cn(
|
||||
'w-full text-left rounded-[5px] border px-3 py-2.5 transition-colors',
|
||||
'hover:bg-elevated',
|
||||
isActive
|
||||
? 'border-accent bg-accent-dim'
|
||||
: 'border-default bg-elevated/50'
|
||||
)}
|
||||
>
|
||||
<p className={cn(
|
||||
'text-sm font-medium',
|
||||
isActive ? 'text-accent-text' : 'text-heading'
|
||||
)}>
|
||||
{branch.label}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Current active step
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -78,7 +78,7 @@ export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps
|
||||
type="button"
|
||||
onClick={() => onClick(branch.id)}
|
||||
className={cn(
|
||||
'w-full text-left rounded-lg border p-2.5 transition-all duration-150',
|
||||
'w-full text-left rounded-lg border p-2.5 transition-all duration-150 cursor-pointer',
|
||||
isActive
|
||||
? cn('bg-card', config.borderClass)
|
||||
: 'bg-card/60 border-default hover:bg-card hover:border-hover',
|
||||
@@ -102,8 +102,12 @@ export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps
|
||||
|
||||
{/* Expanded card positioned over the original */}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onClick(branch.id)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onClick(branch.id) }}
|
||||
className={cn(
|
||||
'absolute z-50 inset-x-0 top-0',
|
||||
'absolute z-50 inset-x-0 top-0 cursor-pointer',
|
||||
'bg-card border rounded-lg p-2.5',
|
||||
'shadow-[0_8px_32px_rgba(0,0,0,0.5)]',
|
||||
config.borderClass,
|
||||
|
||||
@@ -79,9 +79,9 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||||
),
|
||||
// Style horizontal rules
|
||||
hr: () => <hr className="my-4 border-border" />,
|
||||
// Style blockquotes
|
||||
// Style blockquotes — used for AI questions/action items that need to stand out
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="mb-3 border-l-4 border-border pl-4 italic text-muted-foreground last:mb-0">
|
||||
<blockquote className="mb-3 rounded-lg border border-accent/20 bg-accent-dim/50 px-4 py-3 not-italic text-heading last:mb-0">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user