Files
resolutionflow/frontend/src/components/procedural/ResolvedText.tsx
chihlasm ccd06c9ed4 feat: flexible intake — deferred variables + prepared sessions (#103)
* feat: flexible intake — deferred variables + prepared sessions

Remove blocking intake form modal. Variables are now filled inline during
flow execution or pre-filled via prepared sessions. Adds PATCH /sessions/{id}/variables
endpoint, POST /sessions/prepare for session pre-staging, inline variable prompts
in StepDetail, editable Session Variables panel, and "Prepared for You" dashboard section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass treeData directly to startSession to avoid stale state

setTree(treeData) hasn't committed when startSession runs immediately
after, so tree is still null and getStepsFromTree returns []. This
caused the step detail area to render empty on new session start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: wire PrepareSessionModal entry point in Flow Library

Add "Prepare session" button (clipboard icon) to grid, list, and table
views for procedural/maintenance flows. Clicking fetches tree intake
fields and account members, then opens PrepareSessionModal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:49:51 -04:00

88 lines
2.6 KiB
TypeScript

import type { IntakeFormField } from '@/types'
import { InlineVariablePrompt } from './InlineVariablePrompt'
interface ResolvedTextProps {
text: string
variables: Record<string, string>
intakeFields?: IntakeFormField[]
onVariableSubmit?: (variableName: string, value: string) => void
className?: string
}
/**
* Renders text with resolved variables inline. Unresolved [VAR:x] tokens
* are replaced with interactive InlineVariablePrompt components.
*/
export function ResolvedText({
text,
variables,
intakeFields,
onVariableSubmit,
className,
}: ResolvedTextProps) {
if (!text) return null
// Split on variable tokens, keeping the tokens in the result
const tokenPattern = /(\[(?:VAR|USER_INPUT):([^\]]+)\])/g
const parts: Array<{ type: 'text'; value: string } | { type: 'var'; variableName: string; token: string }> = []
let lastIndex = 0
let match
// Remove [SAVE_AS:...] tokens first
const cleaned = text.replace(/\[SAVE_AS:[^\]]+\]/g, '')
while ((match = tokenPattern.exec(cleaned)) !== null) {
// Add text before the token
if (match.index > lastIndex) {
parts.push({ type: 'text', value: cleaned.slice(lastIndex, match.index) })
}
const variableName = match[2].trim()
const resolvedValue = variables[variableName]
if (resolvedValue && resolvedValue.trim()) {
// Variable is resolved — render as plain text
parts.push({ type: 'text', value: resolvedValue })
} else {
// Variable is unresolved — render as inline prompt
parts.push({ type: 'var', variableName, token: match[1] })
}
lastIndex = match.index + match[0].length
}
// Add remaining text
if (lastIndex < cleaned.length) {
parts.push({ type: 'text', value: cleaned.slice(lastIndex) })
}
// If no unresolved variables, just render plain text
const hasUnresolved = parts.some(p => p.type === 'var')
if (!hasUnresolved) {
const fullText = parts.map(p => p.type === 'text' ? p.value : '').join('')
return <span className={className}>{fullText}</span>
}
// Find field metadata helper
const getFieldMeta = (varName: string) =>
intakeFields?.find(f => f.variable_name === varName)
return (
<span className={className}>
{parts.map((part, i) => {
if (part.type === 'text') {
return <span key={i}>{part.value}</span>
}
return (
<InlineVariablePrompt
key={`${part.variableName}-${i}`}
variableName={part.variableName}
fieldMeta={getFieldMeta(part.variableName)}
onSubmit={onVariableSubmit || (() => {})}
/>
)
})}
</span>
)
}