Major UI overhaul plans; Other random docs
This commit is contained in:
259
.agent/skills/speech-to-text/SKILL.md
Normal file
259
.agent/skills/speech-to-text/SKILL.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
---
|
||||||
|
name: speech-to-text
|
||||||
|
description: Transcribe audio to text using ElevenLabs Scribe v2. Use when converting audio/video to text, generating subtitles, transcribing meetings, or processing spoken content.
|
||||||
|
license: MIT
|
||||||
|
compatibility: Requires internet access and an ElevenLabs API key (ELEVENLABS_API_KEY).
|
||||||
|
metadata: {"openclaw": {"requires": {"env": ["ELEVENLABS_API_KEY"]}, "primaryEnv": "ELEVENLABS_API_KEY"}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# ElevenLabs Speech-to-Text
|
||||||
|
|
||||||
|
Transcribe audio to text with Scribe v2 - supports 90+ languages, speaker diarization, and word-level timestamps.
|
||||||
|
|
||||||
|
> **Setup:** See [Installation Guide](references/installation.md). For JavaScript, use `@elevenlabs/*` packages only.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from elevenlabs.client import ElevenLabs
|
||||||
|
|
||||||
|
client = ElevenLabs()
|
||||||
|
|
||||||
|
with open("audio.mp3", "rb") as audio_file:
|
||||||
|
result = client.speech_to_text.convert(file=audio_file, model_id="scribe_v2")
|
||||||
|
|
||||||
|
print(result.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
||||||
|
import { createReadStream } from "fs";
|
||||||
|
|
||||||
|
const client = new ElevenLabsClient();
|
||||||
|
const result = await client.speechToText.convert({
|
||||||
|
file: createReadStream("audio.mp3"),
|
||||||
|
modelId: "scribe_v2",
|
||||||
|
});
|
||||||
|
console.log(result.text);
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.elevenlabs.io/v1/speech-to-text" \
|
||||||
|
-H "xi-api-key: $ELEVENLABS_API_KEY" -F "file=@audio.mp3" -F "model_id=scribe_v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
| Model ID | Description | Best For |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| `scribe_v2` | State-of-the-art accuracy, 90+ languages | Batch transcription, subtitles, long-form audio |
|
||||||
|
| `scribe_v2_realtime` | Low latency (~150ms) | Live transcription, voice agents |
|
||||||
|
|
||||||
|
## Transcription with Timestamps
|
||||||
|
|
||||||
|
Word-level timestamps include type classification and speaker identification:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=audio_file, model_id="scribe_v2", timestamps_granularity="word"
|
||||||
|
)
|
||||||
|
|
||||||
|
for word in result.words:
|
||||||
|
print(f"{word.text}: {word.start}s - {word.end}s (type: {word.type})")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Speaker Diarization
|
||||||
|
|
||||||
|
Identify WHO said WHAT - the model labels each word with a speaker ID, useful for meetings, interviews, or any multi-speaker audio:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=audio_file,
|
||||||
|
model_id="scribe_v2",
|
||||||
|
diarize=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for word in result.words:
|
||||||
|
print(f"[{word.speaker_id}] {word.text}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keyterm Prompting
|
||||||
|
|
||||||
|
Help the model recognize specific words it might otherwise mishear - product names, technical jargon, or unusual spellings (up to 100 terms):
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=audio_file,
|
||||||
|
model_id="scribe_v2",
|
||||||
|
keyterms=["ElevenLabs", "Scribe", "API"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Language Detection
|
||||||
|
|
||||||
|
Automatic detection with optional language hint:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=audio_file,
|
||||||
|
model_id="scribe_v2",
|
||||||
|
language_code="eng" # ISO 639-1 or ISO 639-3 code
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Detected: {result.language_code} ({result.language_probability:.0%})")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Formats
|
||||||
|
|
||||||
|
**Audio:** MP3, WAV, M4A, FLAC, OGG, WebM, AAC, AIFF, Opus
|
||||||
|
**Video:** MP4, AVI, MKV, MOV, WMV, FLV, WebM, MPEG, 3GPP
|
||||||
|
|
||||||
|
**Limits:** Up to 3GB file size, 10 hours duration
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "The full transcription text",
|
||||||
|
"language_code": "eng",
|
||||||
|
"language_probability": 0.98,
|
||||||
|
"words": [
|
||||||
|
{"text": "The", "start": 0.0, "end": 0.15, "type": "word", "speaker_id": "speaker_0"},
|
||||||
|
{"text": " ", "start": 0.15, "end": 0.16, "type": "spacing", "speaker_id": "speaker_0"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Word types:**
|
||||||
|
- `word` - An actual spoken word
|
||||||
|
- `spacing` - Whitespace between words (useful for precise timing)
|
||||||
|
- `audio_event` - Non-speech sounds the model detected (laughter, applause, music, etc.)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
result = client.speech_to_text.convert(file=audio_file, model_id="scribe_v2")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Transcription failed: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Common errors:
|
||||||
|
- **401**: Invalid API key
|
||||||
|
- **422**: Invalid parameters
|
||||||
|
- **429**: Rate limit exceeded
|
||||||
|
|
||||||
|
## Tracking Costs
|
||||||
|
|
||||||
|
Monitor usage via `request-id` response header:
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = client.speech_to_text.convert.with_raw_response(file=audio_file, model_id="scribe_v2")
|
||||||
|
result = response.parse()
|
||||||
|
print(f"Request ID: {response.headers.get('request-id')}")
|
||||||
|
|
||||||
|
## Real-Time Streaming
|
||||||
|
|
||||||
|
For live transcription with ultra-low latency (~150ms), use the real-time API. The real-time API produces two types of transcripts:
|
||||||
|
|
||||||
|
- **Partial transcripts**: Interim results that update frequently as audio is processed - use these for live feedback (e.g., showing text as the user speaks)
|
||||||
|
- **Committed transcripts**: Final, stable results after you "commit" - use these as the source of truth for your application
|
||||||
|
|
||||||
|
A "commit" tells the model to finalize the current segment. You can commit manually (e.g., when the user pauses) or use Voice Activity Detection (VAD) to auto-commit on silence.
|
||||||
|
|
||||||
|
### Python (Server-Side)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from elevenlabs.client import ElevenLabs
|
||||||
|
|
||||||
|
client = ElevenLabs()
|
||||||
|
|
||||||
|
async def transcribe_realtime():
|
||||||
|
async with client.speech_to_text.realtime.connect(
|
||||||
|
model_id="scribe_v2_realtime",
|
||||||
|
include_timestamps=True,
|
||||||
|
) as connection:
|
||||||
|
await connection.stream_url("https://example.com/audio.mp3")
|
||||||
|
|
||||||
|
async for event in connection:
|
||||||
|
if event.type == "partial_transcript":
|
||||||
|
print(f"Partial: {event.text}")
|
||||||
|
elif event.type == "committed_transcript":
|
||||||
|
print(f"Final: {event.text}")
|
||||||
|
|
||||||
|
asyncio.run(transcribe_realtime())
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript (Client-Side with React)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useScribe } from "@elevenlabs/react";
|
||||||
|
|
||||||
|
function TranscriptionComponent() {
|
||||||
|
const [transcript, setTranscript] = useState("");
|
||||||
|
|
||||||
|
const scribe = useScribe({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
onPartialTranscript: (data) => console.log("Partial:", data.text),
|
||||||
|
onCommittedTranscript: (data) => setTranscript((prev) => prev + data.text),
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
// Get token from your backend (never expose API key to client)
|
||||||
|
const { token } = await fetch("/scribe-token").then((r) => r.json());
|
||||||
|
|
||||||
|
await scribe.connect({
|
||||||
|
token,
|
||||||
|
microphone: { echoCancellation: true, noiseSuppression: true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return <button onClick={start}>Start Recording</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Strategies
|
||||||
|
|
||||||
|
| Strategy | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Manual** | You call `commit()` when ready - use for file processing or when you control the audio segments |
|
||||||
|
| **VAD** | Voice Activity Detection auto-commits when silence is detected - use for live microphone input |
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// VAD configuration
|
||||||
|
const connection = await client.speechToText.realtime.connect({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
vad: {
|
||||||
|
silenceThresholdSecs: 1.5,
|
||||||
|
threshold: 0.4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Types
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `partial_transcript` | Live interim results |
|
||||||
|
| `committed_transcript` | Final results after commit |
|
||||||
|
| `committed_transcript_with_timestamps` | Final with word timing |
|
||||||
|
| `error` | Error occurred |
|
||||||
|
|
||||||
|
See real-time references for complete documentation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Installation Guide](references/installation.md)
|
||||||
|
- [Transcription Options](references/transcription-options.md)
|
||||||
|
- [Real-Time Client-Side Streaming](references/realtime-client-side.md)
|
||||||
|
- [Real-Time Server-Side Streaming](references/realtime-server-side.md)
|
||||||
|
- [Commit Strategies](references/realtime-commit-strategies.md)
|
||||||
|
- [Real-Time Event Reference](references/realtime-events.md)
|
||||||
92
.agent/skills/speech-to-text/references/installation.md
Normal file
92
.agent/skills/speech-to-text/references/installation.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
## JavaScript / TypeScript
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @elevenlabs/elevenlabs-js
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** Always use `@elevenlabs/elevenlabs-js`. The old `elevenlabs` npm package (v1.x) is deprecated and should not be used.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
||||||
|
|
||||||
|
// Option 1: Environment variable (recommended)
|
||||||
|
// Set ELEVENLABS_API_KEY in your environment
|
||||||
|
const client = new ElevenLabsClient();
|
||||||
|
|
||||||
|
// Option 2: Pass directly
|
||||||
|
const client = new ElevenLabsClient({ apiKey: "your-api-key" });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrating from deprecated packages
|
||||||
|
|
||||||
|
If you have old packages installed, remove them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove deprecated packages
|
||||||
|
npm uninstall elevenlabs
|
||||||
|
|
||||||
|
# Install the current packages
|
||||||
|
npm install @elevenlabs/elevenlabs-js
|
||||||
|
|
||||||
|
# For client-side/browser usage, also install:
|
||||||
|
npm install @elevenlabs/client # Browser client
|
||||||
|
npm install @elevenlabs/react # React hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import changes:**
|
||||||
|
```javascript
|
||||||
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
||||||
|
import { Scribe } from "@elevenlabs/client";
|
||||||
|
import { useScribe } from "@elevenlabs/react";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install elevenlabs
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from elevenlabs.client import ElevenLabs
|
||||||
|
|
||||||
|
# Option 1: Environment variable (recommended)
|
||||||
|
# Set ELEVENLABS_API_KEY in your environment
|
||||||
|
client = ElevenLabs()
|
||||||
|
|
||||||
|
# Option 2: Pass directly
|
||||||
|
client = ElevenLabs(api_key="your-api-key")
|
||||||
|
```
|
||||||
|
|
||||||
|
## cURL / REST API
|
||||||
|
|
||||||
|
Set your API key as an environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ELEVENLABS_API_KEY="your-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Include in requests via the `xi-api-key` header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.elevenlabs.io/v1/speech-to-text" \
|
||||||
|
-H "xi-api-key: $ELEVENLABS_API_KEY" \
|
||||||
|
-F "file=@audio.mp3" \
|
||||||
|
-F "model_id=scribe_v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting an API Key
|
||||||
|
|
||||||
|
1. Sign up at [elevenlabs.io](https://elevenlabs.io)
|
||||||
|
2. Go to [API Keys](https://elevenlabs.io/app/settings/api-keys)
|
||||||
|
3. Click **Create API Key**
|
||||||
|
4. Copy and store securely
|
||||||
|
|
||||||
|
Or use the `setup-api-key` skill for guided setup.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `ELEVENLABS_API_KEY` | Your ElevenLabs API key (required) |
|
||||||
169
.agent/skills/speech-to-text/references/realtime-client-side.md
Normal file
169
.agent/skills/speech-to-text/references/realtime-client-side.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Client-Side Real-Time Streaming
|
||||||
|
|
||||||
|
Stream audio from the browser directly to ElevenLabs for real-time transcription.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# React
|
||||||
|
npm install @elevenlabs/react @elevenlabs/elevenlabs-js
|
||||||
|
|
||||||
|
# JavaScript
|
||||||
|
npm install @elevenlabs/client @elevenlabs/elevenlabs-js
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning:** Always use the `@elevenlabs/*` namespace for client-side packages.
|
||||||
|
|
||||||
|
## Token Generation
|
||||||
|
|
||||||
|
Client-side streaming requires a single-use token to protect your API key. Generate tokens on your backend:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
||||||
|
|
||||||
|
const elevenlabs = new ElevenLabsClient({
|
||||||
|
apiKey: process.env.ELEVENLABS_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/scribe-token", yourAuthMiddleware, async (req, res) => {
|
||||||
|
const token = await elevenlabs.tokens.singleUse.create("realtime_scribe");
|
||||||
|
res.json(token);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Single-use tokens expire after 15 minutes.
|
||||||
|
|
||||||
|
## React Implementation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useScribe } from "@elevenlabs/react";
|
||||||
|
|
||||||
|
function TranscriptionComponent() {
|
||||||
|
const [transcript, setTranscript] = useState("");
|
||||||
|
|
||||||
|
const scribe = useScribe({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
onPartialTranscript: (data) => {
|
||||||
|
// Show live feedback as user speaks
|
||||||
|
console.log("Partial:", data.text);
|
||||||
|
},
|
||||||
|
onCommittedTranscript: (data) => {
|
||||||
|
// Final transcript for this segment
|
||||||
|
setTranscript((prev) => prev + data.text);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
const tokenResponse = await fetch("/scribe-token");
|
||||||
|
const { token } = await tokenResponse.json();
|
||||||
|
|
||||||
|
await scribe.connect({
|
||||||
|
token,
|
||||||
|
microphone: {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
scribe.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>Status: {scribe.status}</div>
|
||||||
|
<button onClick={startRecording}>Start</button>
|
||||||
|
<button onClick={stopRecording}>Stop</button>
|
||||||
|
<p>{transcript}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Implementation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Scribe, RealtimeEvents } from "@elevenlabs/client";
|
||||||
|
|
||||||
|
async function startTranscription() {
|
||||||
|
const tokenResponse = await fetch("/scribe-token");
|
||||||
|
const { token } = await tokenResponse.json();
|
||||||
|
|
||||||
|
const connection = Scribe.connect({
|
||||||
|
token,
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
includeTimestamps: true,
|
||||||
|
microphone: {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.OPEN, () => {
|
||||||
|
console.log("Connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (data) => {
|
||||||
|
console.log("Partial:", data.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (data) => {
|
||||||
|
console.log("Committed:", data.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (data) => {
|
||||||
|
for (const word of data.words) {
|
||||||
|
console.log(`${word.text}: ${word.start}s - ${word.end}s`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.ERROR, (error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.CLOSE, () => {
|
||||||
|
console.log("Disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Audio Chunking
|
||||||
|
|
||||||
|
For file uploads or custom audio sources, encode to PCM-16 and send in chunks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const chunkSize = 4096;
|
||||||
|
|
||||||
|
for (let offset = 0; offset < pcmData.length; offset += chunkSize) {
|
||||||
|
const chunk = pcmData.slice(offset, offset + chunkSize);
|
||||||
|
const bytes = new Uint8Array(chunk.buffer);
|
||||||
|
const base64 = btoa(String.fromCharCode(...bytes));
|
||||||
|
|
||||||
|
scribe.sendAudio(base64);
|
||||||
|
|
||||||
|
// Simulate real-time streaming
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize transcription
|
||||||
|
scribe.commit();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Microphone Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `echoCancellation` | Remove echo from speakers |
|
||||||
|
| `noiseSuppression` | Filter background noise |
|
||||||
|
| `autoGainControl` | Normalize volume levels |
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Never expose your API key to the client
|
||||||
|
- Always generate single-use tokens on your backend
|
||||||
|
- Use authentication middleware to protect token endpoints
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Transcripts and Commit Strategies
|
||||||
|
|
||||||
|
Control when and how transcripts are finalized in real-time streaming.
|
||||||
|
|
||||||
|
## Why Commits Matter
|
||||||
|
|
||||||
|
In real-time transcription, the model continuously refines its understanding as more audio arrives. A word that sounds like "their" might become "there" or "they're" once more context is heard. The **commit** mechanism lets you decide when to "lock in" the transcript.
|
||||||
|
|
||||||
|
## Transcript Types
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| **Partial** | Interim "best guess" results that update frequently as audio is processed. Use for live feedback (showing text as the user speaks), but don't save these - they may change. |
|
||||||
|
| **Committed** | Final, stable results after a commit occurs. Use these as the source of truth for your application - they won't change. |
|
||||||
|
| **Committed with Timestamps** | Same as committed, but includes word-level timing data for subtitles, karaoke, or lip-sync. |
|
||||||
|
|
||||||
|
## Manual Commit (Default)
|
||||||
|
|
||||||
|
You explicitly control when transcript segments finalize.
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with client.speech_to_text.realtime.connect(
|
||||||
|
model_id="scribe_v2_realtime",
|
||||||
|
) as connection:
|
||||||
|
# Send audio
|
||||||
|
await connection.send({
|
||||||
|
"audio_base_64": audio_base_64,
|
||||||
|
"sample_rate": 16000,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Commit when ready (e.g., pause in speech, end of sentence)
|
||||||
|
await connection.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const connection = await client.speechToText.realtime.connect({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send audio
|
||||||
|
connection.send({
|
||||||
|
audioBase64: audioBase64,
|
||||||
|
sampleRate: 16000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Commit when ready
|
||||||
|
connection.commit();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
- **Commit every 20-30 seconds** for optimal performance
|
||||||
|
- **Commit during silence** or logical breaks (end of sentence, speaker change)
|
||||||
|
- **Auto-commit at 90 seconds** if no manual commit is sent
|
||||||
|
|
||||||
|
### Providing Context
|
||||||
|
|
||||||
|
Send previous text with the first audio chunk to help the model:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await connection.send({
|
||||||
|
"audio_base_64": first_chunk,
|
||||||
|
"sample_rate": 16000,
|
||||||
|
"previous_text": "So as I was saying," # Keep under 50 characters
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This helps with:
|
||||||
|
- Continuing conversations after reconnection
|
||||||
|
- Providing context for better accuracy
|
||||||
|
- Handling sentence fragments
|
||||||
|
|
||||||
|
## Voice Activity Detection (VAD)
|
||||||
|
|
||||||
|
VAD listens for silence and automatically commits when the speaker pauses. This creates natural transcript segments that match how people actually speak - pausing between sentences and thoughts. Recommended for live microphone input.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const connection = await client.speechToText.realtime.connect({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
vad: {
|
||||||
|
silenceThresholdSecs: 1.5, // Silence duration before commit
|
||||||
|
threshold: 0.4, // Speech detection sensitivity (0-1)
|
||||||
|
minSpeechDurationMs: 100, // Minimum speech length required
|
||||||
|
minSilenceDurationMs: 100, // Minimum silence length required
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|-----------|-------------|---------|
|
||||||
|
| `silenceThresholdSecs` | Seconds of silence before auto-commit | 1.5 |
|
||||||
|
| `threshold` | Speech detection sensitivity (lower = more sensitive) | 0.4 |
|
||||||
|
| `minSpeechDurationMs` | Ignore speech shorter than this | 100 |
|
||||||
|
| `minSilenceDurationMs` | Ignore silence shorter than this | 100 |
|
||||||
|
|
||||||
|
### When to Use VAD
|
||||||
|
|
||||||
|
- Live microphone input
|
||||||
|
- Conversational applications
|
||||||
|
- When natural speech boundaries are preferred
|
||||||
|
- Client-side implementations
|
||||||
|
|
||||||
|
### When to Use Manual Commit
|
||||||
|
|
||||||
|
- Processing audio files
|
||||||
|
- Known segment boundaries
|
||||||
|
- Maximum control over timing
|
||||||
|
- Server-side batch processing
|
||||||
|
|
||||||
|
## Supported Audio Formats
|
||||||
|
|
||||||
|
| Format | Sample Rate | Notes |
|
||||||
|
|--------|-------------|-------|
|
||||||
|
| PCM 16-bit | 16kHz | Recommended, best balance |
|
||||||
|
| PCM 16-bit | 8kHz - 48kHz | Supported range |
|
||||||
|
| μ-law 8-bit | 8kHz | Telephony compatibility |
|
||||||
195
.agent/skills/speech-to-text/references/realtime-events.md
Normal file
195
.agent/skills/speech-to-text/references/realtime-events.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Real-Time Event Reference
|
||||||
|
|
||||||
|
Complete reference for events in real-time speech-to-text streaming.
|
||||||
|
|
||||||
|
## Sent Events
|
||||||
|
|
||||||
|
### input_audio_chunk
|
||||||
|
|
||||||
|
Send audio data for transcription.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_type": "input_audio_chunk",
|
||||||
|
"audio_base_64": "<base64-encoded-pcm-audio>",
|
||||||
|
"sample_rate": 16000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `message_type` | string | Always `"input_audio_chunk"` |
|
||||||
|
| `audio_base_64` | string | Base64-encoded PCM audio data |
|
||||||
|
| `sample_rate` | number | Sample rate in Hz (8000-48000) |
|
||||||
|
| `previous_text` | string | Optional context (first chunk only, max 50 chars) |
|
||||||
|
|
||||||
|
### commit
|
||||||
|
|
||||||
|
Finalize the current transcript segment.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_type": "commit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Received Events
|
||||||
|
|
||||||
|
### session_started
|
||||||
|
|
||||||
|
Connection established successfully.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "session_started",
|
||||||
|
"session_id": "abc123",
|
||||||
|
"model_id": "scribe_v2_realtime"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### partial_transcript
|
||||||
|
|
||||||
|
Interim transcription results, updates frequently as audio is processed.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "partial_transcript",
|
||||||
|
"text": "Hello, how are"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `type` | string | `"partial_transcript"` |
|
||||||
|
| `text` | string | Current partial transcription |
|
||||||
|
|
||||||
|
### committed_transcript
|
||||||
|
|
||||||
|
Final transcription after commit.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "committed_transcript",
|
||||||
|
"text": "Hello, how are you today?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `type` | string | `"committed_transcript"` |
|
||||||
|
| `text` | string | Finalized transcription |
|
||||||
|
|
||||||
|
### committed_transcript_with_timestamps
|
||||||
|
|
||||||
|
Final transcription with word-level timing. Sent after `committed_transcript` when `include_timestamps=true`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "committed_transcript_with_timestamps",
|
||||||
|
"words": [
|
||||||
|
{"text": "Hello", "start": 0.0, "end": 0.32, "type": "word"},
|
||||||
|
{"text": ",", "start": 0.32, "end": 0.35, "type": "punctuation"},
|
||||||
|
{"text": " ", "start": 0.35, "end": 0.40, "type": "spacing"},
|
||||||
|
{"text": "how", "start": 0.40, "end": 0.55, "type": "word"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `type` | string | `"committed_transcript_with_timestamps"` |
|
||||||
|
| `words` | array | Word-level timing data |
|
||||||
|
| `words[].text` | string | The word or token |
|
||||||
|
| `words[].start` | number | Start time in seconds |
|
||||||
|
| `words[].end` | number | End time in seconds |
|
||||||
|
| `words[].type` | string | `"word"`, `"spacing"`, `"punctuation"`, `"audio_event"` |
|
||||||
|
|
||||||
|
## Error Events
|
||||||
|
|
||||||
|
### error
|
||||||
|
|
||||||
|
Sent when an error occurs.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"code": "invalid_audio",
|
||||||
|
"message": "Audio format not supported"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Codes
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `authentication_failed` | Invalid API key or token |
|
||||||
|
| `quota_exceeded` | Usage limit reached |
|
||||||
|
| `invalid_audio` | Unsupported audio format |
|
||||||
|
| `rate_limited` | Too many requests |
|
||||||
|
| `session_time_limit_exceeded` | Session exceeded max duration |
|
||||||
|
| `unaccepted_terms` | Terms not accepted in dashboard |
|
||||||
|
| `resource_exhausted` | Server capacity reached |
|
||||||
|
| `transcription_error` | Internal processing error |
|
||||||
|
|
||||||
|
## Connection Events
|
||||||
|
|
||||||
|
### open
|
||||||
|
|
||||||
|
WebSocket connection established.
|
||||||
|
|
||||||
|
### close
|
||||||
|
|
||||||
|
WebSocket connection closed.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "close",
|
||||||
|
"code": 1000,
|
||||||
|
"reason": "Normal closure"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Handling Examples
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
async for event in connection:
|
||||||
|
if event.type == "session_started":
|
||||||
|
print(f"Session: {event.session_id}")
|
||||||
|
elif event.type == "partial_transcript":
|
||||||
|
print(f"Partial: {event.text}")
|
||||||
|
elif event.type == "committed_transcript":
|
||||||
|
print(f"Final: {event.text}")
|
||||||
|
elif event.type == "committed_transcript_with_timestamps":
|
||||||
|
for word in event.words:
|
||||||
|
print(f" {word.text}: {word.start}s - {word.end}s")
|
||||||
|
elif event.type == "error":
|
||||||
|
print(f"Error: {event.code} - {event.message}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
connection.on("session_started", (data) => {
|
||||||
|
console.log("Session:", data.sessionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on("partial_transcript", (data) => {
|
||||||
|
console.log("Partial:", data.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on("committed_transcript", (data) => {
|
||||||
|
console.log("Final:", data.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on("committed_transcript_with_timestamps", (data) => {
|
||||||
|
for (const word of data.words) {
|
||||||
|
console.log(` ${word.text}: ${word.start}s - ${word.end}s`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on("error", (error) => {
|
||||||
|
console.error("Error:", error.code, error.message);
|
||||||
|
});
|
||||||
|
```
|
||||||
316
.agent/skills/speech-to-text/references/realtime-server-side.md
Normal file
316
.agent/skills/speech-to-text/references/realtime-server-side.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# Server-Side Real-Time Streaming
|
||||||
|
|
||||||
|
Transcribe audio streams in real-time from your server with ultra-low latency.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python
|
||||||
|
pip install elevenlabs python-dotenv pydub
|
||||||
|
|
||||||
|
# JavaScript
|
||||||
|
npm install @elevenlabs/elevenlabs-js dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning:** Do not use `npm install elevenlabs` - that's an outdated v1.x package. Always use `@elevenlabs/elevenlabs-js`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Store your API key in a `.env` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
ELEVENLABS_API_KEY=<your_api_key_here>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stream from URL
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
from elevenlabs.client import ElevenLabs
|
||||||
|
from elevenlabs import RealtimeEvents, RealtimeUrlOptions
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
elevenlabs = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
|
||||||
|
stop_event = asyncio.Event()
|
||||||
|
|
||||||
|
connection = await elevenlabs.speech_to_text.realtime.connect(RealtimeUrlOptions(
|
||||||
|
model_id="scribe_v2_realtime",
|
||||||
|
url="https://npr-ice.streamguys1.com/live.mp3",
|
||||||
|
include_timestamps=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
def on_partial_transcript(data):
|
||||||
|
print(f"Partial: {data.get('text', '')}")
|
||||||
|
|
||||||
|
def on_committed_transcript(data):
|
||||||
|
print(f"Committed: {data.get('text', '')}")
|
||||||
|
|
||||||
|
def on_error(error):
|
||||||
|
print(f"Error: {error}")
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
def on_close():
|
||||||
|
print("Connection closed")
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, on_partial_transcript)
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, on_committed_transcript)
|
||||||
|
connection.on(RealtimeEvents.ERROR, on_error)
|
||||||
|
connection.on(RealtimeEvents.CLOSE, on_close)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await stop_event.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopping transcription...")
|
||||||
|
finally:
|
||||||
|
await connection.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import "dotenv/config";
|
||||||
|
import { ElevenLabsClient, RealtimeEvents } from "@elevenlabs/elevenlabs-js";
|
||||||
|
|
||||||
|
const elevenlabs = new ElevenLabsClient();
|
||||||
|
|
||||||
|
const connection = await elevenlabs.speechToText.realtime.connect({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
url: "https://npr-ice.streamguys1.com/live.mp3",
|
||||||
|
includeTimestamps: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (transcript) => {
|
||||||
|
console.log("Partial transcript", transcript);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (transcript) => {
|
||||||
|
console.log("Committed transcript", transcript);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.ERROR, (error) => {
|
||||||
|
console.log("Error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.CLOSE, () => {
|
||||||
|
console.log("Connection closed");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Audio Chunking
|
||||||
|
|
||||||
|
For local files or custom audio streams, convert to PCM format and send in chunks.
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pathlib import Path
|
||||||
|
from elevenlabs.client import ElevenLabs
|
||||||
|
from elevenlabs import AudioFormat, CommitStrategy, RealtimeEvents, RealtimeAudioOptions
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def load_and_convert_audio(audio_path: str | Path, target_sample_rate: int = 16000) -> bytes:
|
||||||
|
if str(audio_path).lower().endswith('.pcm'):
|
||||||
|
with open(audio_path, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
audio = AudioSegment.from_file(audio_path)
|
||||||
|
if audio.channels > 1:
|
||||||
|
audio = audio.set_channels(1)
|
||||||
|
if audio.frame_rate != target_sample_rate:
|
||||||
|
audio = audio.set_frame_rate(target_sample_rate)
|
||||||
|
audio = audio.set_sample_width(2)
|
||||||
|
return audio.raw_data
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
elevenlabs = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
|
||||||
|
transcription_complete = asyncio.Event()
|
||||||
|
|
||||||
|
connection = await elevenlabs.speech_to_text.realtime.connect(RealtimeAudioOptions(
|
||||||
|
model_id="scribe_v2_realtime",
|
||||||
|
audio_format=AudioFormat.PCM_16000,
|
||||||
|
sample_rate=16000,
|
||||||
|
commit_strategy=CommitStrategy.MANUAL,
|
||||||
|
include_timestamps=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
def on_session_started(data):
|
||||||
|
print(f"Session started: {data}")
|
||||||
|
asyncio.create_task(send_audio())
|
||||||
|
|
||||||
|
def on_partial_transcript(data):
|
||||||
|
transcript = data.get('text', '')
|
||||||
|
if transcript:
|
||||||
|
print(f"Partial: {transcript}")
|
||||||
|
|
||||||
|
def on_committed_transcript(data):
|
||||||
|
transcript = data.get('text', '')
|
||||||
|
print(f"\nCommitted transcript: {transcript}")
|
||||||
|
|
||||||
|
def on_committed_transcript_with_timestamps(data):
|
||||||
|
print(f"Timestamps: {data.get('words', '')}")
|
||||||
|
transcription_complete.set()
|
||||||
|
|
||||||
|
def on_error(error):
|
||||||
|
print(f"Error: {error}")
|
||||||
|
transcription_complete.set()
|
||||||
|
|
||||||
|
def on_close():
|
||||||
|
print("Connection closed")
|
||||||
|
transcription_complete.set()
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.SESSION_STARTED, on_session_started)
|
||||||
|
connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, on_partial_transcript)
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, on_committed_transcript)
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, on_committed_transcript_with_timestamps)
|
||||||
|
connection.on(RealtimeEvents.ERROR, on_error)
|
||||||
|
connection.on(RealtimeEvents.CLOSE, on_close)
|
||||||
|
|
||||||
|
async def send_audio():
|
||||||
|
audio_file_path = Path("audio.mp3")
|
||||||
|
audio_data = load_and_convert_audio(audio_file_path)
|
||||||
|
chunk_size = 32000 # 1 second of audio at 16kHz
|
||||||
|
chunks = [audio_data[i:i + chunk_size] for i in range(0, len(audio_data), chunk_size)]
|
||||||
|
|
||||||
|
for i, chunk in enumerate(chunks):
|
||||||
|
chunk_base64 = base64.b64encode(chunk).decode('utf-8')
|
||||||
|
await connection.send({"audio_base_64": chunk_base64, "sample_rate": 16000})
|
||||||
|
|
||||||
|
if i < len(chunks) - 1:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await transcription_complete.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopping...")
|
||||||
|
finally:
|
||||||
|
await connection.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import "dotenv/config";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import { ElevenLabsClient, RealtimeEvents, AudioFormat } from "@elevenlabs/elevenlabs-js";
|
||||||
|
|
||||||
|
const elevenlabs = new ElevenLabsClient();
|
||||||
|
|
||||||
|
const connection = await elevenlabs.speechToText.realtime.connect({
|
||||||
|
modelId: "scribe_v2_realtime",
|
||||||
|
audioFormat: AudioFormat.PCM_16000,
|
||||||
|
sampleRate: 16000,
|
||||||
|
includeTimestamps: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.SESSION_STARTED, (data) => {
|
||||||
|
console.log("Session started", data);
|
||||||
|
sendAudio();
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (transcript) => {
|
||||||
|
console.log("Partial transcript", transcript);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (transcript) => {
|
||||||
|
console.log("Committed transcript", transcript);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (transcript) => {
|
||||||
|
console.log("Committed with timestamps", transcript);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.ERROR, (error) => {
|
||||||
|
console.log("Error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on(RealtimeEvents.CLOSE, () => {
|
||||||
|
console.log("Connection closed");
|
||||||
|
});
|
||||||
|
|
||||||
|
async function sendAudio() {
|
||||||
|
const pcmFilePath = "audio.pcm";
|
||||||
|
const chunkSize = 32000;
|
||||||
|
const audioBuffer = fs.readFileSync(pcmFilePath);
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < audioBuffer.length; i += chunkSize) {
|
||||||
|
const chunk = audioBuffer.subarray(i, i + chunkSize);
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
const chunk = chunks[i];
|
||||||
|
const chunkBase64 = chunk.toString("base64");
|
||||||
|
|
||||||
|
connection.send({
|
||||||
|
audioBase64: chunkBase64,
|
||||||
|
sampleRate: 16000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i < chunks.length - 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
connection.commit();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Direct WebSocket Connection
|
||||||
|
|
||||||
|
For cases where the SDK cannot be used:
|
||||||
|
|
||||||
|
```
|
||||||
|
wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_type": "input_audio_chunk",
|
||||||
|
"audio_base_64": "<base64-encoded-audio>",
|
||||||
|
"sample_rate": 16000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_type": "commit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audio Requirements
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Format | PCM 16-bit |
|
||||||
|
| Sample Rate | 16000 Hz (recommended) |
|
||||||
|
| Channels | Mono |
|
||||||
|
| Chunk Size | 32,000 bytes = 1 second |
|
||||||
|
|
||||||
|
Supported sample rates: 8kHz to 48kHz
|
||||||
174
.agent/skills/speech-to-text/references/transcription-options.md
Normal file
174
.agent/skills/speech-to-text/references/transcription-options.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Transcription Options
|
||||||
|
|
||||||
|
## Request Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|------|----------|-------------|
|
||||||
|
| `file` | file | Yes | Audio or video file to transcribe |
|
||||||
|
| `model_id` | string | Yes | `scribe_v2` (or legacy `scribe_v1`) for batch transcription |
|
||||||
|
| `language_code` | string | No | Language hint (ISO 639-1 or ISO 639-3, e.g., `en` or `eng`) |
|
||||||
|
| `timestamps_granularity` | string | No | `none`, `word`, or `character` (default: `word`) |
|
||||||
|
| `diarize` | boolean | No | Enable speaker diarization (up to 32 speakers for batch) |
|
||||||
|
| `num_speakers` | integer | No | Maximum speakers to detect (up to 32 for batch) |
|
||||||
|
| `diarization_threshold` | number | No | Tune diarization sensitivity when `diarize=true` |
|
||||||
|
| `keyterms` | array | No | Terms to bias transcription (up to 100) |
|
||||||
|
| `tag_audio_events` | boolean | No | Detect non-speech sounds (laughter, applause) |
|
||||||
|
| `entity_detection` | string or array | No | Detect entities (e.g., `pii`, `phi`, `pci`, `offensive_language`) |
|
||||||
|
| `use_multi_channel` | boolean | No | Split multichannel audio into separate transcripts |
|
||||||
|
| `cloud_storage_url` | string | No | HTTPS URL to transcribe instead of uploading a file |
|
||||||
|
| `webhook` | boolean | No | Process async and send result to webhook |
|
||||||
|
| `webhook_metadata` | string or object | No | Custom metadata included in webhook responses |
|
||||||
|
|
||||||
|
## Python Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from elevenlabs.client import ElevenLabs
|
||||||
|
|
||||||
|
client = ElevenLabs()
|
||||||
|
|
||||||
|
with open("audio.mp3", "rb") as audio_file:
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=audio_file,
|
||||||
|
model_id="scribe_v2",
|
||||||
|
language_code="eng",
|
||||||
|
timestamps_granularity="word",
|
||||||
|
diarize=True,
|
||||||
|
keyterms=["ElevenLabs", "Scribe"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
||||||
|
import { createReadStream } from "fs";
|
||||||
|
|
||||||
|
const client = new ElevenLabsClient();
|
||||||
|
|
||||||
|
const result = await client.speechToText.convert({
|
||||||
|
file: createReadStream("audio.mp3"),
|
||||||
|
modelId: "scribe_v2",
|
||||||
|
languageCode: "eng",
|
||||||
|
timestampsGranularity: "word",
|
||||||
|
diarize: true,
|
||||||
|
keyterms: ["ElevenLabs", "Scribe"],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## cURL Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.elevenlabs.io/v1/speech-to-text" \
|
||||||
|
-H "xi-api-key: $ELEVENLABS_API_KEY" \
|
||||||
|
-F "file=@audio.mp3" \
|
||||||
|
-F "model_id=scribe_v2" \
|
||||||
|
-F "language_code=eng" \
|
||||||
|
-F "timestamps_granularity=word" \
|
||||||
|
-F "diarize=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "The complete transcribed text from the audio file.",
|
||||||
|
"language_code": "eng",
|
||||||
|
"language_probability": 0.98,
|
||||||
|
"words": [
|
||||||
|
{
|
||||||
|
"text": "The",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 0.15,
|
||||||
|
"type": "word",
|
||||||
|
"speaker_id": "speaker_0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " ",
|
||||||
|
"start": 0.15,
|
||||||
|
"end": 0.16,
|
||||||
|
"type": "spacing",
|
||||||
|
"speaker_id": "speaker_0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `text` | string | Full transcription text |
|
||||||
|
| `language_code` | string | Detected language (ISO 639-1 or ISO 639-3) |
|
||||||
|
| `language_probability` | float | Confidence in detection (0-1) |
|
||||||
|
| `words` | array | Word-level timestamps (if requested) |
|
||||||
|
| `words[].text` | string | The transcribed word or spacing |
|
||||||
|
| `words[].start` | float | Start time in seconds |
|
||||||
|
| `words[].end` | float | End time in seconds |
|
||||||
|
| `words[].type` | string | `word`, `spacing`, or `audio_event` |
|
||||||
|
| `words[].speaker_id` | string | Speaker identifier (if diarization enabled) |
|
||||||
|
|
||||||
|
## Supported Languages (90+)
|
||||||
|
|
||||||
|
Common languages (ISO 639-3 codes):
|
||||||
|
|
||||||
|
| Code | Language | Code | Language |
|
||||||
|
|------|----------|------|----------|
|
||||||
|
| `eng` | English | `jpn` | Japanese |
|
||||||
|
| `spa` | Spanish | `kor` | Korean |
|
||||||
|
| `fra` | French | `zho` | Mandarin |
|
||||||
|
| `deu` | German | `ara` | Arabic |
|
||||||
|
| `ita` | Italian | `hin` | Hindi |
|
||||||
|
| `por` | Portuguese | `tur` | Turkish |
|
||||||
|
| `nld` | Dutch | `swe` | Swedish |
|
||||||
|
| `pol` | Polish | `dan` | Danish |
|
||||||
|
| `rus` | Russian | `fin` | Finnish |
|
||||||
|
|
||||||
|
Full list: Afrikaans, Amharic, Armenian, Azerbaijani, Belarusian, Bengali, Bosnian, Bulgarian, Burmese, Cantonese, Catalan, Cebuano, Croatian, Czech, Estonian, Filipino, Georgian, Greek, Gujarati, Hausa, Hebrew, Hungarian, Icelandic, Indonesian, Irish, Javanese, Kannada, Kazakh, Khmer, Kyrgyz, Lao, Latvian, Lithuanian, Luxembourgish, Macedonian, Malay, Malayalam, Maltese, Māori, Marathi, Mongolian, Nepali, Norwegian, Odia, Pashto, Persian, Punjabi, Romanian, Serbian, Shona, Sindhi, Slovak, Slovenian, Somali, Swahili, Tamil, Tajik, Telugu, Thai, Ukrainian, Urdu, Uzbek, Vietnamese, Welsh, Wolof, Xhosa, Yoruba, Zulu.
|
||||||
|
|
||||||
|
## Format Requirements
|
||||||
|
|
||||||
|
**Audio:** MP3, WAV, M4A, FLAC, OGG, WebM, AAC, AIFF, Opus
|
||||||
|
**Video:** MP4, AVI, MKV, MOV, WMV, FLV, WebM, MPEG, 3GPP
|
||||||
|
|
||||||
|
**Limits:**
|
||||||
|
- Maximum file size: 3GB
|
||||||
|
- Maximum duration: 10 hours
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Subtitle Generation with Speakers
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=audio_file,
|
||||||
|
model_id="scribe_v2",
|
||||||
|
timestamps_granularity="word",
|
||||||
|
diarize=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate SRT with speaker labels
|
||||||
|
for i, word in enumerate(result.words, 1):
|
||||||
|
if word.type == "word":
|
||||||
|
print(f"[{word.speaker_id}] {word.text} ({word.start:.2f}s)")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meeting Transcription with Custom Terms
|
||||||
|
|
||||||
|
```python
|
||||||
|
with open("meeting.mp3", "rb") as f:
|
||||||
|
result = client.speech_to_text.convert(
|
||||||
|
file=f,
|
||||||
|
model_id="scribe_v2",
|
||||||
|
diarize=True,
|
||||||
|
keyterms=["Q4 forecast", "revenue target", "ACME Corp"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Group by speaker
|
||||||
|
current_speaker = None
|
||||||
|
for word in result.words:
|
||||||
|
if word.type == "word":
|
||||||
|
if word.speaker_id != current_speaker:
|
||||||
|
current_speaker = word.speaker_id
|
||||||
|
print(f"\n[{current_speaker}]:", end=" ")
|
||||||
|
print(word.text, end="")
|
||||||
|
```
|
||||||
28
CLAUDE.md
28
CLAUDE.md
@@ -19,12 +19,28 @@
|
|||||||
| Repository / directory / database / Docker | `patherly` / `patherly_postgres` |
|
| Repository / directory / database / Docker | `patherly` / `patherly_postgres` |
|
||||||
| Backend, frontend UI, production URLs | **ResolutionFlow** |
|
| Backend, frontend UI, production URLs | **ResolutionFlow** |
|
||||||
|
|
||||||
- **Design:** Monochrome dark-only — black backgrounds, white text with opacity, Inter font
|
- **Design:** Dark-first with purple gradient accents (`#818cf8` → `#a78bfa`). NOT monochrome — use gradient for primary buttons, active nav indicators, stat highlights, and brand text.
|
||||||
- **CSS utilities:** `glass-card`, `glass-card-hover`, `glass-card-glow`, `glass-stat` (in `index.css`)
|
- **Fonts:** Plus Jakarta Sans (`font-heading`, headings/titles), Inter (`font-sans`, body text), Outfit (`font-label`, labels/badges/counts) — loaded via Google Fonts
|
||||||
- **Logo:** Inline SVG in `BrandLogo.tsx`
|
- **Logo:** Inline SVG in `BrandLogo.tsx` (decision-tree icon with gradient). Wordmark: "Resolution" in white + "Flow" in `text-gradient-brand`
|
||||||
- **Design system guide:** `docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md`
|
- **Brand assets:** `brand-assets/` (source SVGs + brand-guide.html), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
|
||||||
|
- **CSS utilities:** `text-gradient-brand`, `bg-gradient-brand`, `bg-gradient-brand-hover` (defined in `tailwind.config.js` and `index.css`)
|
||||||
|
- **Layout:** App shell with persistent sidebar + top bar + main workspace (CSS Grid). See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
|
||||||
|
- **Workspace system:** Top-level context switcher (Troubleshooting, Procedures, Policies, Finance). Sidebar categories, tags, stats, and content adapt per workspace. See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
|
||||||
|
- **Rebrand guide:** [REBRAND-IMPLEMENTATION-GUIDE.md](REBRAND-IMPLEMENTATION-GUIDE.md)
|
||||||
|
- **Interactive mockup:** `docs/mockups/resolutionflow-workspaces-mockup.html` (open in browser for visual reference)
|
||||||
|
|
||||||
When adding new pages/components: use "ResolutionFlow" branding, monochrome design, `glass-card` containers, `text-white` hierarchy, white primary buttons, functional color only for status.
|
**Component styling rules:**
|
||||||
|
- Primary buttons: `bg-gradient-brand` with `shadow-lg shadow-primary/20`, hover lifts with stronger shadow
|
||||||
|
- Secondary buttons: `bg-card` with `border-border`, hover brightens border
|
||||||
|
- Active nav items: `bg-primary/8` background + 3px left gradient accent bar
|
||||||
|
- Stat values: use `text-gradient-brand` for highlighted metrics
|
||||||
|
- Status colors: green (`text-green-500`) for success, amber (`text-amber-500`) for in-progress, red (`text-red-500`) for error/critical
|
||||||
|
- Category dots: 8px colored circles using the category color palette
|
||||||
|
- Tags/badges: `font-label` (Outfit), small rounded chips with `bg-card border-border`
|
||||||
|
- Cards: `bg-card border-border rounded-xl`, hover brightens border
|
||||||
|
- Section labels: `font-label text-[0.6875rem] uppercase tracking-wide text-muted-foreground`
|
||||||
|
|
||||||
|
When adding new pages/components: use "ResolutionFlow" branding, purple gradient accent theme, `bg-card` containers, `text-foreground`/`text-muted-foreground` hierarchy. Primary actions use `bg-gradient-brand`. Pages render inside the app shell (CSS Grid: topbar + sidebar + main). Reference [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) for layout patterns, workspace context, and component specs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,7 +77,7 @@ When adding new pages/components: use "ResolutionFlow" branding, monochrome desi
|
|||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- **Framework:** React 19 + Vite + TypeScript
|
- **Framework:** React 19 + Vite + TypeScript
|
||||||
- **Styling:** Tailwind CSS v3 — monochrome glass-morphism (dark-only)
|
- **Styling:** Tailwind CSS v3 — dark-first with purple gradient accents (see Branding section)
|
||||||
- **State:** Zustand (with immer + zundo for undo/redo)
|
- **State:** Zustand (with immer + zundo for undo/redo)
|
||||||
- **Routing:** React Router v7
|
- **Routing:** React Router v7
|
||||||
- **API Client:** Axios with token refresh interceptor
|
- **API Client:** Axios with token refresh interceptor
|
||||||
|
|||||||
858
UI-DESIGN-SYSTEM.md
Normal file
858
UI-DESIGN-SYSTEM.md
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
# ResolutionFlow UI Design System & Layout Architecture
|
||||||
|
|
||||||
|
> **Purpose:** This document defines the new app-shell layout, design tokens, component patterns, and workspace architecture for ResolutionFlow. It is the single source of truth for all frontend UI work going forward.
|
||||||
|
> **Last Updated:** February 15, 2026
|
||||||
|
> **Status:** Approved for implementation
|
||||||
|
> **Reference Mockup:** `docs/mockups/resolutionflow-workspaces-mockup.html` (interactive, open in browser)
|
||||||
|
> **Note:** If the mockup file is not yet in the repo, it should be placed at the path above. The mockup is also available as a project output artifact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Layout Architecture](#1-layout-architecture)
|
||||||
|
2. [Design Tokens](#2-design-tokens)
|
||||||
|
3. [Workspace System](#3-workspace-system)
|
||||||
|
4. [Sidebar Components](#4-sidebar-components)
|
||||||
|
5. [Top Bar](#5-top-bar)
|
||||||
|
6. [Main Content Area](#6-main-content-area)
|
||||||
|
7. [Component Patterns](#7-component-patterns)
|
||||||
|
8. [Icon System](#8-icon-system)
|
||||||
|
9. [Animation & Transitions](#9-animation--transitions)
|
||||||
|
10. [Data Model Changes](#10-data-model-changes)
|
||||||
|
11. [Migration Strategy](#11-migration-strategy)
|
||||||
|
12. [Implementation Phases](#12-implementation-phases)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Layout Architecture
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
ResolutionFlow transitions from a top-nav + full-width content layout to a **persistent sidebar + top bar + main workspace** layout. This mirrors the UX patterns MSP engineers already use in ConnectWise Automate, Datto RMM, and HaloPSA.
|
||||||
|
|
||||||
|
### Shell Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ TOP BAR (56px) — Logo, Search, Quick Actions, User │
|
||||||
|
├──────────────┬───────────────────────────────────────┤
|
||||||
|
│ SIDEBAR │ MAIN CONTENT AREA │
|
||||||
|
│ (260px) │ │
|
||||||
|
│ │ Page Header + Actions │
|
||||||
|
│ Workspace │ Quick Stats Row │
|
||||||
|
│ Switcher │ Filters Bar │
|
||||||
|
│ │ Content Sections │
|
||||||
|
│ Navigation │ (trees/flows list, sessions, etc.) │
|
||||||
|
│ │ │
|
||||||
|
│ Categories │ │
|
||||||
|
│ Tags │ │
|
||||||
|
│ │ │
|
||||||
|
│ ─────────── │ │
|
||||||
|
│ Team │ │
|
||||||
|
│ Settings │ │
|
||||||
|
└──────────────┴───────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Grid Implementation
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// AppLayout.tsx - new shell structure
|
||||||
|
<div className="app-shell">
|
||||||
|
<header className="topbar">...</header>
|
||||||
|
<nav className="sidebar">...</nav>
|
||||||
|
<main className="main-content">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.app-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr;
|
||||||
|
grid-template-rows: 56px 1fr;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
grid-column: 1 / -1; /* spans full width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Variable for Sidebar Width
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--sidebar-w: 260px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This variable should be used consistently for sidebar width, logo area width calculations, and responsive breakpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Design Tokens
|
||||||
|
|
||||||
|
### Existing Tokens (Already in tailwind.config.js and index.css)
|
||||||
|
|
||||||
|
These are already implemented and MUST be used. Do NOT introduce new color values that duplicate or conflict with these.
|
||||||
|
|
||||||
|
#### Brand Colors (from tailwind.config.js)
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| `brand.gradient.from` | `#818cf8` | Gradient start, primary accent |
|
||||||
|
| `brand.gradient.to` | `#a78bfa` | Gradient end |
|
||||||
|
| `brand.dark.DEFAULT` | `#09090b` | App background (dark mode) |
|
||||||
|
| `brand.dark.card` | `#18181b` | Card backgrounds |
|
||||||
|
| `brand.dark.surface` | `#12121c` | Elevated surface |
|
||||||
|
| `brand.text.primary` | `#ffffff` | Primary text |
|
||||||
|
| `brand.text.secondary` | `#a1a1aa` | Secondary text |
|
||||||
|
| `brand.text.muted` | `#52525b` | Muted/dim text |
|
||||||
|
| `brand.border` | `#27272a` | Standard borders |
|
||||||
|
|
||||||
|
#### Tailwind Utilities (from index.css)
|
||||||
|
|
||||||
|
| Utility | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `bg-gradient-brand` | `linear-gradient(90deg, #818cf8, #a78bfa)` |
|
||||||
|
| `bg-gradient-brand-hover` | `linear-gradient(90deg, #6366f1, #9333ea)` |
|
||||||
|
| `text-gradient-brand` | Gradient text effect (uses bg-clip-text) |
|
||||||
|
| `font-heading` | Plus Jakarta Sans |
|
||||||
|
| `font-sans` | Inter |
|
||||||
|
| `font-label` | Outfit |
|
||||||
|
|
||||||
|
#### shadcn/ui CSS Variables (from index.css)
|
||||||
|
|
||||||
|
All shadcn component tokens are defined via HSL CSS variables (`--background`, `--foreground`, `--card`, `--primary`, etc.) with both light and dark mode values. Continue using these for shadcn components.
|
||||||
|
|
||||||
|
### NEW Design Tokens for App Shell
|
||||||
|
|
||||||
|
These are already added to `index.css` within the `:root` block (the app is dark-only, all tokens live in `:root`):
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Existing tokens... */
|
||||||
|
|
||||||
|
/* App Shell tokens */
|
||||||
|
--sidebar-bg: 240 10% 4.5%; /* #0c0c0f - slightly lighter than app bg */
|
||||||
|
--sidebar-hover: 240 6% 12%; /* #1e1e23 */
|
||||||
|
--sidebar-active: 243 75% 59% / 0.08; /* brand purple at 8% opacity */
|
||||||
|
--border-subtle: 240 6% 12%; /* #1f1f23 - lighter border for sections */
|
||||||
|
--text-dimmed: 240 4% 24%; /* #3f3f46 - very dim text for timestamps */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Semantic Status Colors
|
||||||
|
|
||||||
|
These map to session/tree status indicators and MUST be used consistently:
|
||||||
|
|
||||||
|
| Status | Color | Tailwind Class | Usage |
|
||||||
|
|--------|-------|---------------|-------|
|
||||||
|
| Success / Resolved | `#22c55e` | `text-green-500` / `bg-green-500` | Completed sessions, resolved |
|
||||||
|
| Warning / In Progress | `#f59e0b` | `text-amber-500` / `bg-amber-500` | Open/active sessions |
|
||||||
|
| Error / Critical | `#ef4444` | `text-red-500` / `bg-red-500` | Failed, critical priority |
|
||||||
|
| Info | `#3b82f6` | `text-blue-500` / `bg-blue-500` | Informational, default |
|
||||||
|
|
||||||
|
### Category Colors
|
||||||
|
|
||||||
|
Categories use a fixed palette of distinguishable colors for their dot indicators:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// constants/categoryColors.ts
|
||||||
|
export const CATEGORY_COLORS = [
|
||||||
|
'#3b82f6', // blue
|
||||||
|
'#22c55e', // green
|
||||||
|
'#f59e0b', // amber
|
||||||
|
'#ef4444', // red
|
||||||
|
'#8b5cf6', // violet
|
||||||
|
'#06b6d4', // cyan
|
||||||
|
'#ec4899', // pink
|
||||||
|
'#f97316', // orange
|
||||||
|
'#14b8a6', // teal
|
||||||
|
'#6366f1', // indigo
|
||||||
|
] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Categories are assigned colors based on their creation order (index % length). Colors are stored on the category model in the database.
|
||||||
|
|
||||||
|
> **Migration needed:** The `tree_categories` table already has `display_order` and `is_active` columns. Add a `color` column:
|
||||||
|
> ```sql
|
||||||
|
> ALTER TABLE tree_categories ADD COLUMN color VARCHAR(7) DEFAULT '#3b82f6';
|
||||||
|
> ```
|
||||||
|
> Backfill existing categories with colors from the palette based on their `display_order`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Workspace System
|
||||||
|
|
||||||
|
### Concept
|
||||||
|
|
||||||
|
Workspaces are the top-level organizational context. They sit **above** the existing folder system — a workspace scopes which trees/flows are visible, while user folders remain available within each workspace for personal organization. When a user switches workspace, the sidebar categories, tags, stats, filters, and main content all adapt.
|
||||||
|
|
||||||
|
> **Important:** Workspaces do NOT replace the existing `UserFolder` system. Folders are per-user personal organization that continue to work within the active workspace context. The folder sidebar, drag-and-drop, and folder-tree junction table are preserved. Workspaces filter what content is available; folders organize it.
|
||||||
|
|
||||||
|
The layout, navigation structure, and component patterns stay identical — only the DATA changes.
|
||||||
|
|
||||||
|
### Workspace Types (Initial Set)
|
||||||
|
|
||||||
|
| Workspace | Description | Icon | Accent Color |
|
||||||
|
|-----------|-------------|------|-------------|
|
||||||
|
| Troubleshooting | Break/fix decision trees | 🔧 | `#ef4444` (red) |
|
||||||
|
| Procedures | Step-by-step operational flows | 📋 | `#3b82f6` (blue) |
|
||||||
|
| Policies | Compliance & policy builders | 📜 | `#8b5cf6` (violet) |
|
||||||
|
| Finance | Billing & procurement flows | 💰 | `#22c55e` (green) |
|
||||||
|
|
||||||
|
**Users and admins can create custom workspaces.** The four above are defaults.
|
||||||
|
|
||||||
|
### Workspace Switcher Component
|
||||||
|
|
||||||
|
Located at the top of the sidebar, below the nav section label. It's a dropdown that shows:
|
||||||
|
|
||||||
|
1. **Current workspace** — icon, name, description, chevron
|
||||||
|
2. **Dropdown options** — all available workspaces with flow counts
|
||||||
|
3. **"Add workspace…"** — link at bottom to create new
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 🔧 Troubleshooting │
|
||||||
|
│ Break/fix decision trees │
|
||||||
|
│ ▼ │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 🔧 Troubleshooting 42 │ ← active (highlighted)
|
||||||
|
│ 📋 Procedures 18 │
|
||||||
|
│ 📜 Policies 7 │
|
||||||
|
│ 💰 Finance 4 │
|
||||||
|
│─────────────────────────────│
|
||||||
|
│ + Add workspace… │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Behavior on Workspace Switch
|
||||||
|
|
||||||
|
When the user selects a new workspace:
|
||||||
|
|
||||||
|
1. Sidebar content fades out (200ms opacity transition)
|
||||||
|
2. Data updates:
|
||||||
|
- Categories list → filtered to workspace
|
||||||
|
- Tags cloud → filtered to workspace
|
||||||
|
- Nav badge counts → reflect workspace totals
|
||||||
|
- "All Trees" label → changes (e.g., "All Procedures")
|
||||||
|
- "Tree Editor" label → changes (e.g., "Flow Editor")
|
||||||
|
- Search placeholder → adapts
|
||||||
|
3. Main content fades out/in (200ms opacity transition)
|
||||||
|
- Page title changes
|
||||||
|
- Stats row updates
|
||||||
|
- Filter chips update
|
||||||
|
- Tree/flow list filters to workspace
|
||||||
|
- Sessions panel filters to workspace
|
||||||
|
4. "New Tree" button label → adapts (e.g., "New Procedure")
|
||||||
|
5. Toast notification confirms switch: "Switched to **Procedures**"
|
||||||
|
|
||||||
|
### Data Model
|
||||||
|
|
||||||
|
See [Section 10: Data Model Changes](#10-data-model-changes) for backend schema.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Sidebar Components
|
||||||
|
|
||||||
|
### Structure (top to bottom)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Workspace Switcher (dropdown)
|
||||||
|
2. ─── divider ───
|
||||||
|
3. Primary Navigation
|
||||||
|
- Dashboard (grid icon)
|
||||||
|
- All Trees/Flows (cube icon) [badge: count]
|
||||||
|
- Tree/Flow Editor (pencil icon)
|
||||||
|
- Sessions (clock icon) [badge: active count]
|
||||||
|
- Exports (file icon)
|
||||||
|
- Step Library (bookmark icon) [dot: new items]
|
||||||
|
4. ─── divider ───
|
||||||
|
5. Categories Section
|
||||||
|
- Section label: "CATEGORIES"
|
||||||
|
- Category items with color dots and counts
|
||||||
|
6. ─── divider ───
|
||||||
|
7. Tags Section
|
||||||
|
- Section label: "POPULAR TAGS"
|
||||||
|
- Tag chips in a flex-wrap cloud
|
||||||
|
8. ─── spacer (flex-grow) ───
|
||||||
|
9. Footer (pinned to bottom)
|
||||||
|
- Team (users icon)
|
||||||
|
- Settings (gear icon)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nav Item Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface NavItemProps {
|
||||||
|
href: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
badge?: number | 'dot';
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Active state styling:**
|
||||||
|
- Background: `var(--sidebar-active)` (brand purple at 8% opacity)
|
||||||
|
- Left border accent: 3px gradient bar (brand gradient, rounded right corners)
|
||||||
|
- Text: `text-foreground` (white in dark mode)
|
||||||
|
- Icon: full opacity
|
||||||
|
|
||||||
|
**Inactive state styling:**
|
||||||
|
- Background: transparent
|
||||||
|
- Text: `text-muted-foreground`
|
||||||
|
- Icon: 70% opacity
|
||||||
|
- Hover: `var(--sidebar-hover)` background
|
||||||
|
|
||||||
|
### Category Item Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface CategoryItemProps {
|
||||||
|
name: string;
|
||||||
|
color: string; // hex color for the dot
|
||||||
|
count: number;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders as: `[●] Category Name .............. 12`
|
||||||
|
|
||||||
|
- 8px color dot (rounded full)
|
||||||
|
- Name in `text-sm text-muted-foreground`
|
||||||
|
- Count right-aligned in `font-label text-xs` (Outfit font)
|
||||||
|
- Hover: `var(--sidebar-hover)` background, text brightens
|
||||||
|
|
||||||
|
### Tag Cloud Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface TagCloudProps {
|
||||||
|
tags: string[];
|
||||||
|
onTagClick: (tag: string) => void;
|
||||||
|
activeTags?: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders as flex-wrap container of small chip elements:
|
||||||
|
- Background: `bg-card`
|
||||||
|
- Border: `border-border`
|
||||||
|
- Text: `font-label text-xs` (Outfit)
|
||||||
|
- Active: purple-tinted background and border
|
||||||
|
- Gap: 4px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Top Bar
|
||||||
|
|
||||||
|
### Structure (left to right)
|
||||||
|
|
||||||
|
```
|
||||||
|
[Logo + Wordmark] | [Search Input (⌘K)] | ──flex grow── | [Quick Launch] [Notifications] [Avatar]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logo Area
|
||||||
|
|
||||||
|
- Width: `calc(var(--sidebar-w) - 40px)` — aligns with sidebar content
|
||||||
|
- Contains: ResolutionFlow logo SVG (32x32) + branded wordmark
|
||||||
|
- Wordmark: "Resolution" in white + "Flow" in gradient text
|
||||||
|
|
||||||
|
### Search Bar
|
||||||
|
|
||||||
|
- Flex: 1, max-width 480px
|
||||||
|
- Placeholder adapts to workspace context (e.g., "Search trees, sessions, tags…")
|
||||||
|
- Left icon: magnifying glass (Lucide `Search`)
|
||||||
|
- Right badge: keyboard shortcut hint `⌘K` (styled as small pill)
|
||||||
|
- Border: `border-border`, focus: `border-brand-gradient-from`
|
||||||
|
|
||||||
|
### Action Buttons
|
||||||
|
|
||||||
|
| Button | Icon | Behavior |
|
||||||
|
|--------|------|----------|
|
||||||
|
| Quick Launch | `Zap` (Lucide) | Opens quick-launch modal to start a session fast |
|
||||||
|
| Notifications | `Bell` (Lucide) | Opens notification panel. Red dot badge when unread |
|
||||||
|
| User Avatar | Initials circle | Opens user menu dropdown (profile, settings, logout) |
|
||||||
|
|
||||||
|
Avatar: 32px circle with brand gradient background, white initials in `font-heading text-xs font-bold`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Main Content Area
|
||||||
|
|
||||||
|
### Page Structure
|
||||||
|
|
||||||
|
Every page in the main content follows this vertical flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Page Header (title + action buttons)
|
||||||
|
2. Quick Stats Row (4 cards)
|
||||||
|
3. Filters Bar (chips)
|
||||||
|
4. Content Sections (grouped lists, panels)
|
||||||
|
```
|
||||||
|
|
||||||
|
Not every page needs all sections. The Dashboard uses all four. The Tree Editor page would only use the page header and then the editor workspace.
|
||||||
|
|
||||||
|
### Quick Stats Row
|
||||||
|
|
||||||
|
4 stat cards in a CSS grid (`grid-template-columns: repeat(4, 1fr)`).
|
||||||
|
|
||||||
|
Each card:
|
||||||
|
- Background: `bg-card`
|
||||||
|
- Border: `border` (subtle, brightens on hover)
|
||||||
|
- Border radius: `rounded-xl` (10px)
|
||||||
|
- Padding: 16px 18px
|
||||||
|
- Label: `font-label text-[0.6875rem] uppercase tracking-wide text-muted-foreground`
|
||||||
|
- Value: `font-heading text-2xl font-bold`
|
||||||
|
- Meta: `text-[0.6875rem] text-[var(--text-dimmed)]`
|
||||||
|
|
||||||
|
Special value styles:
|
||||||
|
- `.gradient` → `text-gradient-brand`
|
||||||
|
- Colored → direct color style (e.g., amber for warnings)
|
||||||
|
|
||||||
|
### Filters Bar
|
||||||
|
|
||||||
|
Horizontal row of filter chips with a divider before "More Filters":
|
||||||
|
|
||||||
|
```
|
||||||
|
[All*] [Recently Used] [My Trees] [Team Trees] [Defaults] | [⊗ More Filters]
|
||||||
|
```
|
||||||
|
|
||||||
|
Active chip: purple-tinted background/border (same as sidebar tag active state).
|
||||||
|
|
||||||
|
### Section Groups
|
||||||
|
|
||||||
|
Collapsible sections with header:
|
||||||
|
|
||||||
|
```
|
||||||
|
[●] SECTION TITLE 5 [▼]
|
||||||
|
─────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
- Dot: 8px gradient circle
|
||||||
|
- Title: `font-heading text-[0.8125rem] font-bold uppercase tracking-wide`
|
||||||
|
- Count: pill with `bg-surface` background
|
||||||
|
- Collapse: chevron button (toggles section visibility)
|
||||||
|
|
||||||
|
### Tree/Flow List Items
|
||||||
|
|
||||||
|
Grid layout per item:
|
||||||
|
|
||||||
|
```
|
||||||
|
grid-template-columns: 40px 1fr 130px 80px 100px 40px
|
||||||
|
icon info category uses updated actions
|
||||||
|
```
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
- Background: `bg-card`, transparent border
|
||||||
|
- Hover: `border-border`, `bg-[var(--sidebar-hover)]`
|
||||||
|
- Border radius: `rounded-lg`
|
||||||
|
- Padding: 12px 16px
|
||||||
|
|
||||||
|
**Icon box:** 36x36px rounded-lg with category-tinted background and emoji.
|
||||||
|
|
||||||
|
**Info column:**
|
||||||
|
- Name: `font-heading text-sm font-semibold`
|
||||||
|
- Meta row: tags (small chips) + step/solution count
|
||||||
|
|
||||||
|
**Category column:** color dot + name in `font-label text-xs`
|
||||||
|
|
||||||
|
**Actions:** Three-dot menu, opacity 0 until row hover.
|
||||||
|
|
||||||
|
### Sessions Panel
|
||||||
|
|
||||||
|
Contained card with header and rows:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌ Recent Sessions ──────── [View All] ┐
|
||||||
|
│ ● DNS Resolution Failure → step 4/12 TKT-4821 12 min ago │
|
||||||
|
│ ● Account Lockout → step 6/18 TKT-4819 45 min ago │
|
||||||
|
│ ✓ Mail Flow ✓ Resolved TKT-4815 Yesterday │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Session row grid:
|
||||||
|
```
|
||||||
|
grid-template-columns: 8px 1fr 140px 80px 100px
|
||||||
|
dot name progress ticket time
|
||||||
|
```
|
||||||
|
|
||||||
|
Status dot colors: `bg-amber-500` (in-progress), `bg-green-500` (completed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Component Patterns
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
| Variant | Background | Text | Shadow | Hover |
|
||||||
|
|---------|-----------|------|--------|-------|
|
||||||
|
| Primary | `bg-gradient-brand` | white | `shadow-lg shadow-primary/20` | Lift + stronger shadow |
|
||||||
|
| Secondary | `bg-card` | `text-muted-foreground` | border only | Border brightens |
|
||||||
|
|
||||||
|
Both: `rounded-lg px-4 py-2 text-sm font-semibold`, with icon support (16px Lucide icon + 6px gap).
|
||||||
|
|
||||||
|
### Badges
|
||||||
|
|
||||||
|
- **Count badge:** `bg-card border border-border rounded-full px-2 text-[0.6875rem] font-label`
|
||||||
|
- **Active count badge:** purple-tinted background/border
|
||||||
|
- **Dot badge:** 6px circle, `bg-brand-gradient-from`
|
||||||
|
- **Notification badge:** 8px red circle with sidebar-bg border (creates "cut out" effect)
|
||||||
|
|
||||||
|
### Toast Notifications
|
||||||
|
|
||||||
|
Fixed-position toast notification (current app uses top-right via Sonner — keep this position for consistency):
|
||||||
|
- Background: `bg-card`
|
||||||
|
- Border: brand gradient border
|
||||||
|
- Border radius: `rounded-xl`
|
||||||
|
- Shadow: `shadow-xl` with heavy dark shadow
|
||||||
|
- Content: emoji icon + message text (supports `<strong>` for emphasis)
|
||||||
|
- Auto-dismiss: 2000ms with fade out
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Icon System
|
||||||
|
|
||||||
|
### Primary Source: Lucide React
|
||||||
|
|
||||||
|
All icons use [Lucide React](https://lucide.dev) at consistent sizes:
|
||||||
|
|
||||||
|
| Context | Size | Lucide Prop |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| Nav items | 18px | `size={18}` |
|
||||||
|
| Buttons | 16px | `size={16}` |
|
||||||
|
| Top bar actions | 18px | `size={18}` |
|
||||||
|
| Section headers | 14px | `size={14}` |
|
||||||
|
| Filter chips | 14px | `size={14}` |
|
||||||
|
|
||||||
|
### Navigation Icon Mapping
|
||||||
|
|
||||||
|
| Nav Item | Lucide Icon |
|
||||||
|
|----------|-------------|
|
||||||
|
| Dashboard | `LayoutGrid` |
|
||||||
|
| All Trees/Flows | `Box` |
|
||||||
|
| Tree/Flow Editor | `PenLine` |
|
||||||
|
| Sessions | `Clock` |
|
||||||
|
| Exports | `FileText` |
|
||||||
|
| Step Library | `Bookmark` |
|
||||||
|
| Team | `Users` |
|
||||||
|
| Settings | `Settings` |
|
||||||
|
| Search | `Search` |
|
||||||
|
| Quick Launch | `Zap` |
|
||||||
|
| Notifications | `Bell` |
|
||||||
|
| New Tree | `Plus` |
|
||||||
|
| Import | `Upload` |
|
||||||
|
| Filter | `Filter` |
|
||||||
|
| Collapse | `ChevronDown` |
|
||||||
|
| More Actions | `MoreHorizontal` |
|
||||||
|
|
||||||
|
### Category Emoji Icons
|
||||||
|
|
||||||
|
Categories in the tree list use emoji as visual anchors inside tinted icon boxes. These are set per-tree or per-category and stored as a string field.
|
||||||
|
|
||||||
|
Default emoji per common categories:
|
||||||
|
- Networking: 🌐
|
||||||
|
- Active Directory: 🔒
|
||||||
|
- Email/Exchange: 📧
|
||||||
|
- Server Issues: 🖥️
|
||||||
|
- VPN/Remote: 🔌
|
||||||
|
- Citrix/RDS: 🖥️
|
||||||
|
- Printers: 🖨️
|
||||||
|
- Backup/DR: 🔄
|
||||||
|
- Onboarding: 👤
|
||||||
|
- Security: 🔐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Animation & Transitions
|
||||||
|
|
||||||
|
### Page/Section Load
|
||||||
|
|
||||||
|
Staggered fade-in using CSS animation:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply with incrementing `animation-delay` on child sections:
|
||||||
|
- Stats row: 50ms
|
||||||
|
- Filters: 100ms
|
||||||
|
- Section 1: 150ms
|
||||||
|
- Section 2: 200ms
|
||||||
|
|
||||||
|
### Workspace Switch
|
||||||
|
|
||||||
|
Coordinated fade transition:
|
||||||
|
1. Sidebar content: `opacity → 0` (200ms)
|
||||||
|
2. Main content: `opacity → 0` (200ms)
|
||||||
|
3. Data swap (instant)
|
||||||
|
4. Both: `opacity → 1` (200ms)
|
||||||
|
|
||||||
|
### Interactive Elements
|
||||||
|
|
||||||
|
| Element | Property | Duration | Easing |
|
||||||
|
|---------|----------|----------|--------|
|
||||||
|
| Nav item hover | background-color | 120ms | ease |
|
||||||
|
| Button hover | transform, box-shadow | 150ms | ease |
|
||||||
|
| Tree item hover | background, border-color | 120ms | ease |
|
||||||
|
| Dropdown open | opacity, transform | 150ms | ease |
|
||||||
|
| Action buttons (three-dot) | opacity | 120ms | ease |
|
||||||
|
| Toast appear/disappear | opacity, transform | 300ms | ease |
|
||||||
|
|
||||||
|
### Workspace Dropdown
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes dropIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Data Model Changes
|
||||||
|
|
||||||
|
### New: `workspaces` Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workspaces (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
icon VARCHAR(10), -- emoji character
|
||||||
|
accent_color VARCHAR(7), -- hex color (e.g., '#ef4444')
|
||||||
|
account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
is_default BOOLEAN DEFAULT FALSE,
|
||||||
|
sort_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default workspaces (seeded):**
|
||||||
|
|
||||||
|
| name | slug | icon | accent_color | is_default |
|
||||||
|
|------|------|------|-------------|------------|
|
||||||
|
| Troubleshooting | troubleshooting | 🔧 | #ef4444 | true |
|
||||||
|
| Procedures | procedures | 📋 | #3b82f6 | false |
|
||||||
|
| Policies | policies | 📜 | #8b5cf6 | false |
|
||||||
|
| Finance | finance | 💰 | #22c55e | false |
|
||||||
|
|
||||||
|
### Modified: `trees` Table
|
||||||
|
|
||||||
|
Add column:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE trees ADD COLUMN workspace_id UUID REFERENCES workspaces(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
All existing trees get assigned to the "troubleshooting" workspace via data migration.
|
||||||
|
|
||||||
|
> **Note:** The `trees` table already has a `tree_type` column with values `'troubleshooting'` and `'procedural'`. The workspace system is a superset of this — `tree_type` controls tree behavior (branching vs linear), while `workspace_id` controls organizational context. Both fields coexist. The data migration should map existing `tree_type='troubleshooting'` trees to the Troubleshooting workspace and `tree_type='procedural'` trees to the Procedures workspace.
|
||||||
|
|
||||||
|
### Modified: `tree_categories` Table
|
||||||
|
|
||||||
|
Add column:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE tree_categories ADD COLUMN workspace_id UUID REFERENCES workspaces(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
Categories become workspace-scoped. A category belongs to one workspace. The existing `account_id` column remains for tenancy — `workspace_id` is an additional organizational dimension within an account.
|
||||||
|
|
||||||
|
### Modified: `tree_tags` Table
|
||||||
|
|
||||||
|
Tags remain global (cross-workspace) but the popular tags query filters by workspace context. The existing `account_id` scoping on tags is preserved.
|
||||||
|
|
||||||
|
### API Changes
|
||||||
|
|
||||||
|
**New endpoints:**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/workspaces -- list all workspaces for user's account
|
||||||
|
POST /api/v1/workspaces -- create workspace (admin)
|
||||||
|
PATCH /api/v1/workspaces/{id} -- update workspace
|
||||||
|
DELETE /api/v1/workspaces/{id} -- soft delete workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modified endpoints:**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/trees?workspace_id={uuid} -- add workspace filter
|
||||||
|
GET /api/v1/categories?workspace_id={uuid} -- categories are a separate router (categories.py)
|
||||||
|
GET /api/v1/sessions?workspace_id={uuid}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Categories have their own dedicated router at `/api/v1/categories` (not nested under `/trees/categories`). Tags are at `/api/v1/tags`. Folders are at `/api/v1/folders`. See `backend/app/api/router.py` for the full route registry.
|
||||||
|
|
||||||
|
### Frontend State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// store/workspaceStore.ts
|
||||||
|
interface WorkspaceState {
|
||||||
|
workspaces: Workspace[];
|
||||||
|
activeWorkspaceId: string | null;
|
||||||
|
setActiveWorkspace: (id: string) => void;
|
||||||
|
// Persisted in localStorage as 'active-workspace-id'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All tree/session/category queries include the active workspace ID as a filter parameter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Migration Strategy
|
||||||
|
|
||||||
|
### From Current Layout to App Shell
|
||||||
|
|
||||||
|
The current `AppLayout.tsx` uses a top-nav bar with horizontal nav links and an `<Outlet>` for full-width page content. The migration:
|
||||||
|
|
||||||
|
1. **AppLayout.tsx** → Complete rewrite to CSS Grid shell (topbar + sidebar + main)
|
||||||
|
2. **BrandLogo + BrandWordmark** → Move into topbar logo area
|
||||||
|
3. **Nav links** → Move into sidebar as vertical nav items with icons
|
||||||
|
4. **ThemeToggle** → Move into sidebar footer or top bar
|
||||||
|
5. **User menu** → Avatar in top bar with dropdown
|
||||||
|
6. **Outlet** → Renders inside `<main>` grid cell
|
||||||
|
|
||||||
|
### Page-by-Page Adaptation
|
||||||
|
|
||||||
|
| Page | Changes Needed |
|
||||||
|
|------|---------------|
|
||||||
|
| TreeLibraryPage | Becomes the "Dashboard" or "All Trees" view in main content area. Add stats row, filters, and grouped list layout |
|
||||||
|
| TreeNavigationPage | Renders in main content. Scratchpad overlay unchanged (fixed position) |
|
||||||
|
| TreeEditorPage | Renders in main content. Full-width editor workspace |
|
||||||
|
| SessionHistoryPage | Renders in main content. Add filters and session list |
|
||||||
|
| SessionDetailPage | Renders in main content. Minimal changes |
|
||||||
|
| SettingsPage | Renders in main content. Minimal changes |
|
||||||
|
| LoginPage / RegisterPage | **No change** — these render outside the app shell (no sidebar) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Implementation Phases
|
||||||
|
|
||||||
|
### Phase A: Foundation (Backend + Shell)
|
||||||
|
|
||||||
|
1. Create `workspace` table, model, schemas, migration
|
||||||
|
2. Add `workspace_id` to `tree` and `tree_category` tables
|
||||||
|
3. Seed default workspaces
|
||||||
|
4. Data migration: assign all existing trees to "troubleshooting" workspace
|
||||||
|
5. Create workspace API endpoints
|
||||||
|
6. Create `workspaceStore` (Zustand + localStorage)
|
||||||
|
7. **Rewrite `AppLayout.tsx`** to CSS Grid shell with topbar + sidebar + main
|
||||||
|
8. Move existing nav items into sidebar
|
||||||
|
9. Add workspace switcher component (static, no data yet)
|
||||||
|
|
||||||
|
### Phase B: Sidebar Components
|
||||||
|
|
||||||
|
1. Build `NavItem` component with active state + badge
|
||||||
|
2. Build `CategoryList` component with color dots
|
||||||
|
3. Build `TagCloud` component
|
||||||
|
4. Build `WorkspaceSwitcher` dropdown with animations
|
||||||
|
5. Wire workspace switcher to `workspaceStore`
|
||||||
|
6. Update tree/session queries to include workspace filter
|
||||||
|
|
||||||
|
### Phase C: Main Content Redesign
|
||||||
|
|
||||||
|
1. Build `QuickStats` row component
|
||||||
|
2. Build `FiltersBar` component
|
||||||
|
3. Build `SectionGroup` collapsible component
|
||||||
|
4. Build `TreeListItem` component (grid layout)
|
||||||
|
5. Build `SessionsPanel` component
|
||||||
|
6. Redesign `TreeLibraryPage` as Dashboard using new components
|
||||||
|
7. Add search bar with ⌘K shortcut
|
||||||
|
|
||||||
|
### Phase D: Polish & Integration
|
||||||
|
|
||||||
|
1. Add workspace switch animations (fade transitions)
|
||||||
|
2. Add toast notifications for workspace switch
|
||||||
|
3. Responsive breakpoints (collapse sidebar on mobile)
|
||||||
|
4. Update all page components to work within new shell
|
||||||
|
5. E2E testing of workspace switching flow
|
||||||
|
6. Performance testing (workspace switch should be <200ms)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A: File Structure for New Components
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/src/
|
||||||
|
├── components/
|
||||||
|
│ ├── layout/
|
||||||
|
│ │ ├── AppLayout.tsx ← REWRITE (CSS Grid shell)
|
||||||
|
│ │ ├── TopBar.tsx ← NEW
|
||||||
|
│ │ ├── Sidebar.tsx ← NEW
|
||||||
|
│ │ ├── NavItem.tsx ← NEW
|
||||||
|
│ │ └── ProtectedRoute.tsx (unchanged)
|
||||||
|
│ ├── workspace/
|
||||||
|
│ │ ├── WorkspaceSwitcher.tsx ← NEW
|
||||||
|
│ │ ├── CategoryList.tsx ← NEW
|
||||||
|
│ │ └── TagCloud.tsx ← NEW
|
||||||
|
│ ├── dashboard/
|
||||||
|
│ │ ├── QuickStats.tsx ← NEW
|
||||||
|
│ │ ├── FiltersBar.tsx ← NEW
|
||||||
|
│ │ ├── SectionGroup.tsx ← NEW
|
||||||
|
│ │ ├── TreeListItem.tsx ← NEW
|
||||||
|
│ │ └── SessionsPanel.tsx ← NEW
|
||||||
|
│ └── common/
|
||||||
|
│ ├── Toast.tsx ← NEW
|
||||||
|
│ └── ... (existing)
|
||||||
|
├── store/
|
||||||
|
│ ├── workspaceStore.ts ← NEW
|
||||||
|
│ └── ... (existing)
|
||||||
|
├── api/
|
||||||
|
│ ├── workspaces.ts ← NEW
|
||||||
|
│ └── ... (existing)
|
||||||
|
├── types/
|
||||||
|
│ ├── workspace.ts ← NEW
|
||||||
|
│ └── ... (existing)
|
||||||
|
└── constants/
|
||||||
|
└── categoryColors.ts ← NEW
|
||||||
|
```
|
||||||
|
|
||||||
|
## Appendix B: Typography Quick Reference
|
||||||
|
|
||||||
|
| Element | Font | Weight | Size | Tracking |
|
||||||
|
|---------|------|--------|------|----------|
|
||||||
|
| Page titles | Plus Jakarta Sans (`font-heading`) | 700 | 1.375rem (22px) | -0.01em |
|
||||||
|
| Section titles | Plus Jakarta Sans | 700 | 0.8125rem (13px) | 0.04em, uppercase |
|
||||||
|
| Nav items | Inter (`font-sans`) | 500 | 0.8125rem (13px) | normal |
|
||||||
|
| Tree names | Plus Jakarta Sans | 600 | 0.875rem (14px) | -0.005em |
|
||||||
|
| Stat values | Plus Jakarta Sans | 700 | 1.5rem (24px) | -0.02em |
|
||||||
|
| Stat labels | Outfit (`font-label`) | 600 | 0.6875rem (11px) | 0.05em, uppercase |
|
||||||
|
| Badges/counts | Outfit | 400 | 0.6875rem (11px) | normal |
|
||||||
|
| Timestamps | Inter | 400 | 0.6875rem (11px) | normal |
|
||||||
|
| Tags/chips | Outfit | 400 | 0.625rem (10px) | normal |
|
||||||
|
| Search input | Inter | 400 | 0.8125rem (13px) | normal |
|
||||||
|
|
||||||
|
## Appendix C: Workspace-Specific Adaptations
|
||||||
|
|
||||||
|
When a workspace is active, these elements adapt:
|
||||||
|
|
||||||
|
| Element | Troubleshooting | Procedures | Policies | Finance |
|
||||||
|
|---------|----------------|------------|----------|---------|
|
||||||
|
| "All ___" nav label | All Trees | All Procedures | All Policies | All Finance Flows |
|
||||||
|
| Editor nav label | Tree Editor | Flow Editor | Policy Editor | Flow Editor |
|
||||||
|
| New button label | New Tree | New Procedure | New Policy | New Flow |
|
||||||
|
| Search placeholder | Search trees, sessions, tags… | Search procedures, runbooks… | Search policies, compliance… | Search billing, procurement… |
|
||||||
|
| Stat 1 label | Active Trees | Active Procedures | Active Policies | Active Flows |
|
||||||
|
| Stat 2 label | Sessions Today | Runs This Week | Pending Review | Runs This Month |
|
||||||
|
| Stat 3 label | Open Sessions | In Progress | Compliance Score | Cost Saved |
|
||||||
|
| Stat 4 label | Docs Generated | Avg Completion | Last Audit | Pending Approvals |
|
||||||
1107
docs/mockups/resolutionflow-workspaces-mockup.html
Normal file
1107
docs/mockups/resolutionflow-workspaces-mockup.html
Normal file
File diff suppressed because it is too large
Load Diff
458
docs/plans/2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md
Normal file
458
docs/plans/2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
# Export & Ticket Note Improvements — Merged Specification
|
||||||
|
|
||||||
|
> **Date:** February 13, 2026
|
||||||
|
> **Status:** Draft — Pending Implementation
|
||||||
|
> **Source:** Merged from two independent improvement proposals
|
||||||
|
> **Scope:** Backend export generators, frontend export UX, session model changes
|
||||||
|
> **Dependencies:** Existing session export system (`sessions.py`), `SessionExport` schema, `TreeNavigationPage`, `SessionDetailPage`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
ResolutionFlow's export system currently treats sessions as all-or-nothing: either a session is complete and exports fully, or it's incomplete and exports with no indication of status, missing context, or next steps. This creates several gaps for MSP engineers:
|
||||||
|
|
||||||
|
1. **No mid-session export** — Engineers pulled away mid-troubleshooting can't grab progress notes from the active navigation page without leaving the flow.
|
||||||
|
2. **Outcome notes silently dropped** — Engineers write outcome notes in the completion modal, but these never appear in exported ticket notes.
|
||||||
|
3. **No follow-up / next steps section** — MSP tickets almost always need follow-up actions documented, but the export has no dedicated field for this.
|
||||||
|
4. **No export control granularity** — No way to export a subset of steps, control verbosity, or review/redact content before copying to a ticket system.
|
||||||
|
5. **Custom steps not differentiated** — Steps added by the engineer during a session (via Step Library) look identical to tree-authored steps in the export.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Summary
|
||||||
|
|
||||||
|
| # | Feature | Priority | Effort | Source |
|
||||||
|
|---|---------|----------|--------|--------|
|
||||||
|
| 1 | Mid-session export from TreeNavigationPage | High | Medium | Proposal 2 |
|
||||||
|
| 2 | Partial export with step cutoff | High | Small | Proposal 1 |
|
||||||
|
| 3 | Include outcome_notes in export | High | Small | Both |
|
||||||
|
| 4 | Next Steps / Follow-Up field + export section | High | Medium | Proposal 2 + new DB field |
|
||||||
|
| 5 | Structured Ticket Summary block | Medium | Medium | Proposal 1 |
|
||||||
|
| 6 | Custom step differentiation in export | Medium | Small | Proposal 2 |
|
||||||
|
| 7 | Verbosity / detail level controls | Medium | Medium | Proposal 1 |
|
||||||
|
| 8 | Editable preview before copy | Medium | Medium | Both |
|
||||||
|
| 9 | Sensitive data review / redaction | Low | Large | Proposal 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase A: Core Export Gaps (High Priority)
|
||||||
|
|
||||||
|
These address missing data in exports — things that should be there today but aren't.
|
||||||
|
|
||||||
|
#### A1. Include Outcome Notes in Export
|
||||||
|
|
||||||
|
**Problem:** `outcome_notes` is captured during session completion but not rendered in any export format.
|
||||||
|
|
||||||
|
**Current state:** The session model already has `outcome_notes` as a `Text` column, and `SessionComplete` already accepts `outcome_notes: Optional[str]`. However, none of the four export generators (`generate_markdown_export`, `generate_text_export`, `generate_html_export`, `generate_psa_export`) in `backend/app/services/export_service.py` reference it.
|
||||||
|
|
||||||
|
> **VERIFIED:** `outcome_notes` exists on the Session model and SessionComplete schema. No migration needed for this field — only export generator changes.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Backend:** Add an "Outcome / Resolution Notes" section to all four export generators (markdown, text, HTML, PSA), rendered after the Troubleshooting Steps section and before any Next Steps section. Only render if `outcome_notes` is non-empty. For PSA format, update the existing `--- RESOLUTION ---` section to use `outcome_notes` instead of the last decision's answer.
|
||||||
|
- **Schema:** Add `include_outcome_notes: bool = True` to `SessionExport`.
|
||||||
|
|
||||||
|
**Markdown output example:**
|
||||||
|
```markdown
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
Replaced failed DIMM in slot A2. Memtest passed 3 cycles post-replacement.
|
||||||
|
Server returned to production at 14:45.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### A2. Next Steps / Follow-Up Field
|
||||||
|
|
||||||
|
**Problem:** MSP tickets almost always need follow-up actions documented ("Monitor for 24 hours", "Schedule firmware update for maintenance window", "Escalate to vendor if recurs"), but there is no field for this in the session model or the export.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Database:** Add `next_steps` column to the `sessions` table as a new `Text` field (nullable, `server_default=sa.text("''")`), following the same pattern as the `scratchpad` column.
|
||||||
|
- **Migration:** Alembic migration to add the column with backfill of existing rows.
|
||||||
|
- **Schema:** Add `next_steps: Optional[str] = None` to `SessionUpdate`. Add `next_steps: str = ""` with normalizing validator to `SessionResponse`.
|
||||||
|
- **Frontend — Completion modal:** Add a "Next Steps / Follow-Up" text area to the session completion flow, so engineers can capture follow-up actions at the same time they write outcome notes.
|
||||||
|
- **Frontend — Session detail:** Display next steps in the session detail view.
|
||||||
|
- **Backend — Export:** Add a "Next Steps" section to all four export generators (markdown, text, HTML, PSA), rendered after the Resolution section. Only render if non-empty.
|
||||||
|
|
||||||
|
**Markdown output example:**
|
||||||
|
```markdown
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Monitor Event Log for Event ID 41 recurrence over next 48 hours
|
||||||
|
- Schedule firmware update for next maintenance window (Feb 20)
|
||||||
|
- If issue recurs, escalate to Dell ProSupport case #SR-4482991
|
||||||
|
```
|
||||||
|
|
||||||
|
#### A3. Mid-Session Export from TreeNavigationPage
|
||||||
|
|
||||||
|
**Problem:** Export currently only works from `SessionDetailPage` after navigating away from the tree. If an engineer gets pulled away mid-troubleshooting, they can't grab what they've done so far without leaving the flow.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Frontend — TreeNavigationPage:** Add a "Copy for Ticket" button (same pattern as the existing one on `SessionDetailPage`) that exports steps completed up to the current point.
|
||||||
|
- **Export behavior:** Uses the existing export endpoint but the session is still in-progress. The export should:
|
||||||
|
- Include a `**Status:** In Progress` indicator in the header metadata.
|
||||||
|
- Show "Session still in progress — exported at step N of path" note.
|
||||||
|
- Include all decisions recorded so far.
|
||||||
|
- Include scratchpad content captured so far.
|
||||||
|
- NOT mark the session as `exported = True` (since it's still active).
|
||||||
|
- **Backend:** The export endpoint currently sets `session.exported = True` unconditionally. Add logic: only set `exported = True` if the session has a `completed_at` timestamp. Alternatively, add an `include_in_progress_header: bool = False` option to `SessionExport` that the TreeNavigationPage sets to `True`.
|
||||||
|
|
||||||
|
**UX detail:** The button should be accessible but not prominent — engineers shouldn't accidentally think it ends their session. A secondary/outline-style button with a clipboard icon in the navigation page toolbar is appropriate.
|
||||||
|
|
||||||
|
#### A4. Partial Export with Step Cutoff
|
||||||
|
|
||||||
|
**Problem:** When reviewing a completed (or abandoned) session, there's no way to export only the first N steps — useful for escalation handoff snapshots.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Schema:** Add `max_step_index: Optional[int] = None` to `SessionExport`. This is 1-based and inclusive (e.g., `max_step_index=3` exports steps 1, 2, and 3).
|
||||||
|
- **Backend:** All four export generators slice the `session.decisions` list by `max_step_index` before iterating.
|
||||||
|
- **Validation:** If `max_step_index` is provided, it must be >= 1. Values greater than the actual decision count are clamped to the full list (no error). Zero or negative values return a 422 validation error.
|
||||||
|
- **Frontend — SessionDetailPage:** Add an "Export through step N" dropdown/slider control in the export options area. Default: all steps (no cutoff).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase B: Export Quality & Readability (Medium Priority)
|
||||||
|
|
||||||
|
These improve the usefulness and polish of exports for their audience (dispatchers, managers, ticket reviewers).
|
||||||
|
|
||||||
|
#### B1. Structured Ticket Summary Block
|
||||||
|
|
||||||
|
**Problem:** Exports dive straight into step-by-step detail, which is great for engineers reviewing their own work but overwhelming for dispatchers and managers who need a quick overview.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Schema:** Add `include_summary: bool = False` to `SessionExport`.
|
||||||
|
- **Backend:** When enabled, generate a "Summary" section at the top of the export (after metadata, before Evidence/Steps) with these fields:
|
||||||
|
- **Issue:** Auto-populated from tree name/description.
|
||||||
|
- **Impact:** Blank by default (user-editable in preview).
|
||||||
|
- **Current Status:** "Resolved" if `completed_at` is set, otherwise "In Progress — paused at step N".
|
||||||
|
- **Resolution:** Auto-populated from `outcome_notes` if available.
|
||||||
|
- **Next Steps:** Auto-populated from `next_steps` field if available.
|
||||||
|
- **Frontend:** When summary is enabled, show an editable preview of these fields before export, allowing the engineer to fill in blanks or adjust auto-populated values.
|
||||||
|
|
||||||
|
**Markdown output example:**
|
||||||
|
```markdown
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Field | Detail |
|
||||||
|
|-------|--------|
|
||||||
|
| **Issue** | VPN Connection Failure |
|
||||||
|
| **Impact** | User unable to access internal resources remotely |
|
||||||
|
| **Status** | Resolved |
|
||||||
|
| **Resolution** | DNS misconfiguration on VPN adapter — updated DNS servers |
|
||||||
|
| **Next Steps** | Monitor for recurrence over 48 hours |
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B2. Custom Step Differentiation in Export
|
||||||
|
|
||||||
|
**Problem:** When engineers add custom steps during a session (via the Step Library), those steps are stored in `custom_steps` but the export treats all decisions identically. Ticket reviewers have no visibility into where the engineer deviated from the standard path.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Backend:** In all four export generators, detect custom steps by checking if `node_id` starts with `custom-` (this is the canonical marker — custom steps always use `custom-{uuid}` as their node_id).
|
||||||
|
- **Markdown format:** Prefix custom steps with `[CUSTOM]` in the heading and add an italic note.
|
||||||
|
- **HTML format:** Add a visual badge/tag (styled like the frontend's purple custom step badge).
|
||||||
|
- **Text format:** Prefix with `[CUSTOM]` label.
|
||||||
|
|
||||||
|
**Markdown output example:**
|
||||||
|
```markdown
|
||||||
|
### Step 5: [CUSTOM] Check Additional Event Logs
|
||||||
|
*Custom step added by engineer*
|
||||||
|
|
||||||
|
**Action:** Reviewed Application log for correlated errors
|
||||||
|
**Notes:** Found repeated .NET runtime errors starting 2 hours before reported issue
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B3. Verbosity / Detail Level Controls
|
||||||
|
|
||||||
|
**Problem:** Exports can become extremely long when sessions include command outputs, detailed scratchpad notes, and many steps. Pasting a wall of text into a ConnectWise ticket isn't practical.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Schema:** Add `detail_level: Literal["summary", "standard", "full"] = "standard"` to `SessionExport`.
|
||||||
|
- **Backend behavior by level:**
|
||||||
|
- **summary:** Header metadata + Summary block (auto-enabled) + Resolution + Next Steps. No individual step details. Designed for management/dispatch visibility.
|
||||||
|
- **standard:** Everything in summary, plus all troubleshooting steps with notes. Command outputs longer than 5 lines are truncated with "*(full output omitted)*". This is the default and matches current behavior plus new sections.
|
||||||
|
- **full:** Everything included with no truncation. Command outputs, scratchpad, all notes rendered in full. Designed for detailed review or archival.
|
||||||
|
- **Frontend:** Add a detail level selector (3-option toggle or dropdown) in the export options area on both `SessionDetailPage` and `TreeNavigationPage`.
|
||||||
|
|
||||||
|
#### B4. Editable Preview Before Copy
|
||||||
|
|
||||||
|
**Problem:** The "Copy for Ticket" button generates content and copies it to the clipboard immediately with no chance to review. Engineers often need to clean up notes, add context, or remove sensitive info before pasting into a PSA.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Frontend — ExportPreviewModal enhancement:** Instead of the current read-only preview modal, make the preview content editable. The flow becomes:
|
||||||
|
1. Engineer clicks "Copy for Ticket" or "Preview".
|
||||||
|
2. Modal opens with generated export content in an editable text area.
|
||||||
|
3. Engineer reviews and optionally edits the content.
|
||||||
|
4. Engineer clicks "Copy" (copies the edited version) or "Download" (saves the edited version).
|
||||||
|
- **Important:** Edits in the preview are NOT saved back to the session. This is a one-way "edit before copy" flow.
|
||||||
|
- **The existing "Copy" button** (direct copy without preview) should remain available for engineers who want the quick path. The preview/edit step is opt-in.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase C: Advanced Features (Lower Priority)
|
||||||
|
|
||||||
|
#### C1. Sensitive Data Review / Redaction
|
||||||
|
|
||||||
|
**Problem:** Scratchpad and command outputs encourage capturing detailed data (IPs, hostnames, tokens, account IDs), but ticket systems often need sanitized notes. Engineers currently have to manually scan and redact before pasting.
|
||||||
|
|
||||||
|
**Changes required:**
|
||||||
|
|
||||||
|
- **Schema:** Add `redaction_mode: Literal["none", "mask"] = "none"` to `SessionExport`.
|
||||||
|
- **Backend — Redaction pipeline:** When `redaction_mode="mask"`, apply regex-based detection and masking of common sensitive patterns before returning export content:
|
||||||
|
- IPv4/IPv6 addresses → `[IP REDACTED]`
|
||||||
|
- Email addresses → `[EMAIL REDACTED]`
|
||||||
|
- Common token/key patterns (API keys, bearer tokens) → `[TOKEN REDACTED]`
|
||||||
|
- UNC paths and hostnames → optionally masked (configurable)
|
||||||
|
- **Frontend — Preview integration:** When the editable preview modal is open, add a toggle for "Show sensitive data highlights." When enabled, likely sensitive values are visually highlighted (yellow background or similar). A "Mask All" button applies redaction. Individual items can be toggled on/off.
|
||||||
|
- **Copy actions:** "Copy" copies current content as-is. "Copy Redacted" applies the masking pipeline to whatever is currently in the editor.
|
||||||
|
|
||||||
|
**Implementation note:** Start with a conservative set of regex patterns. False positives (masking things that aren't sensitive) are annoying but safe. False negatives (missing real sensitive data) are the actual risk. The editable preview (B4) gives engineers a safety net regardless.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
### New Database Columns
|
||||||
|
|
||||||
|
| Column | Table | Type | Default | Migration Required |
|
||||||
|
|--------|-------|------|---------|--------------------|
|
||||||
|
| `outcome_notes` | sessions | Text, nullable | `''` | No — already exists |
|
||||||
|
| `next_steps` | sessions | Text, nullable | `''` | Yes |
|
||||||
|
|
||||||
|
Both columns follow the same pattern as the existing `scratchpad` column: `Text` type, nullable, `server_default=sa.text("''")`, with backfill of existing rows in the migration.
|
||||||
|
|
||||||
|
### Schema Changes — `SessionExport`
|
||||||
|
|
||||||
|
Current:
|
||||||
|
```python
|
||||||
|
class SessionExport(BaseModel):
|
||||||
|
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
|
||||||
|
include_timestamps: bool = True
|
||||||
|
include_tree_info: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Updated:
|
||||||
|
```python
|
||||||
|
class SessionExport(BaseModel):
|
||||||
|
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
|
||||||
|
include_timestamps: bool = True
|
||||||
|
include_tree_info: bool = True
|
||||||
|
|
||||||
|
# Phase A additions
|
||||||
|
include_outcome_notes: bool = True
|
||||||
|
max_step_index: Optional[int] = Field(None, ge=1, description="1-based inclusive step cutoff")
|
||||||
|
|
||||||
|
# Phase B additions
|
||||||
|
include_summary: bool = False
|
||||||
|
detail_level: Literal["summary", "standard", "full"] = "standard"
|
||||||
|
|
||||||
|
# Phase C additions
|
||||||
|
redaction_mode: Literal["none", "mask"] = "none"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Changes — `SessionUpdate` and `SessionResponse`
|
||||||
|
|
||||||
|
Add to `SessionUpdate`:
|
||||||
|
```python
|
||||||
|
outcome_notes: Optional[str] = None
|
||||||
|
next_steps: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `SessionResponse`:
|
||||||
|
```python
|
||||||
|
outcome_notes: str = ""
|
||||||
|
next_steps: str = ""
|
||||||
|
|
||||||
|
@validator('outcome_notes', 'next_steps', pre=True, always=True)
|
||||||
|
def normalize_text_fields(cls, v):
|
||||||
|
return v or ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Completion Endpoint
|
||||||
|
|
||||||
|
Update `POST /sessions/{id}/complete` to accept:
|
||||||
|
```python
|
||||||
|
class SessionComplete(BaseModel):
|
||||||
|
outcome_notes: Optional[str] = None
|
||||||
|
next_steps: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export Output Structure (All Formats)
|
||||||
|
|
||||||
|
After all phases, the full export structure in order:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Header Metadata (tree name, ticket #, client, timestamps, status)
|
||||||
|
2. Summary Block (Phase B1, optional — enabled by include_summary or detail_level=summary)
|
||||||
|
3. Evidence / Reference (existing scratchpad section)
|
||||||
|
4. Troubleshooting Steps (existing, enhanced with custom step markers and step cutoff)
|
||||||
|
5. Resolution / Outcome Notes (Phase A1)
|
||||||
|
6. Next Steps / Follow-Up (Phase A2)
|
||||||
|
7. Session Duration (existing timestamp-derived, shown when include_timestamps=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Markdown Example — Complete Session, Standard Detail
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# VPN Connection Failure
|
||||||
|
|
||||||
|
**Ticket:** SR-2847
|
||||||
|
**Client:** Contoso Ltd
|
||||||
|
**Started:** 2026-02-13 09:15
|
||||||
|
**Completed:** 2026-02-13 09:42
|
||||||
|
**Status:** Resolved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evidence / Reference
|
||||||
|
|
||||||
|
- Server IP: 10.0.1.50
|
||||||
|
- VPN Client: GlobalProtect 6.2.1
|
||||||
|
- Affected user: jsmith@contoso.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Steps
|
||||||
|
|
||||||
|
### Step 1: Is the VPN client installed and up to date?
|
||||||
|
**Answer:** Yes
|
||||||
|
**Notes:** GlobalProtect 6.2.1 confirmed
|
||||||
|
|
||||||
|
### Step 2: Can the user reach the VPN gateway?
|
||||||
|
**Answer:** Yes
|
||||||
|
**Notes:** Ping to vpn.contoso.com successful
|
||||||
|
|
||||||
|
### Step 3: Check DNS configuration on VPN adapter
|
||||||
|
**Answer:** DNS servers incorrect
|
||||||
|
**Notes:** VPN adapter had 8.8.8.8 instead of internal DC
|
||||||
|
|
||||||
|
### Step 4: [CUSTOM] Verify DNS propagation after fix
|
||||||
|
*Custom step added by engineer*
|
||||||
|
**Action:** Ran nslookup against internal resources
|
||||||
|
**Notes:** All internal names resolving correctly after DNS update
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
Updated DNS configuration on VPN adapter to point to internal DCs (10.0.1.10, 10.0.1.11).
|
||||||
|
Flushed DNS cache. Verified internal name resolution working. User confirmed full access restored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Monitor for recurrence over next 48 hours
|
||||||
|
- Check if GPO is failing to push correct DNS settings to this machine
|
||||||
|
- Schedule follow-up with user on Friday if no recurrence
|
||||||
|
```
|
||||||
|
|
||||||
|
### Markdown Example — In-Progress Session (Mid-Session Export)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# VPN Connection Failure
|
||||||
|
|
||||||
|
**Ticket:** SR-2847
|
||||||
|
**Client:** Contoso Ltd
|
||||||
|
**Started:** 2026-02-13 09:15
|
||||||
|
**Status:** In Progress (exported at step 3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evidence / Reference
|
||||||
|
|
||||||
|
- Server IP: 10.0.1.50
|
||||||
|
- VPN Client: GlobalProtect 6.2.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Steps
|
||||||
|
|
||||||
|
### Step 1: Is the VPN client installed and up to date?
|
||||||
|
**Answer:** Yes
|
||||||
|
|
||||||
|
### Step 2: Can the user reach the VPN gateway?
|
||||||
|
**Answer:** Yes
|
||||||
|
|
||||||
|
### Step 3: Check DNS configuration on VPN adapter
|
||||||
|
**Answer:** DNS servers incorrect
|
||||||
|
**Notes:** VPN adapter had 8.8.8.8 instead of internal DC — investigating
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Changes Summary
|
||||||
|
|
||||||
|
| Page | Change | Phase |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `TreeNavigationPage` | Add "Copy for Ticket" button with in-progress export | A |
|
||||||
|
| `SessionDetailPage` | Add step cutoff control to export options | A |
|
||||||
|
| `SessionDetailPage` | Add detail level selector | B |
|
||||||
|
| `ExportPreviewModal` | Make preview content editable before copy | B |
|
||||||
|
| `ExportPreviewModal` | Add sensitive data highlighting and mask toggle | C |
|
||||||
|
| Session completion modal | Add "Next Steps" text area | A |
|
||||||
|
| Session completion modal | Ensure outcome_notes are saved (verify current behavior) | A |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### Phase A Tests
|
||||||
|
|
||||||
|
1. **Outcome notes in export** — Completed session with outcome_notes includes "Resolution" section in markdown, text, and HTML formats. Session without outcome_notes omits the section cleanly.
|
||||||
|
2. **Next steps in export** — Session with next_steps includes "Next Steps" section in all formats. Empty next_steps omits the section.
|
||||||
|
3. **Mid-session export** — Exporting an in-progress session includes "In Progress" status, completed steps only, and does NOT set `exported = True`.
|
||||||
|
4. **Partial export** — `max_step_index=3` returns only first 3 decisions. `max_step_index` greater than decision count returns all decisions (no error). `max_step_index=0` or negative returns 422.
|
||||||
|
5. **Backward compatibility** — Existing export calls with no new parameters produce identical output to current behavior. All existing export tests continue to pass.
|
||||||
|
|
||||||
|
### Phase B Tests
|
||||||
|
|
||||||
|
6. **Summary block** — `include_summary=True` adds Summary table at top with auto-populated fields.
|
||||||
|
7. **Custom step marking** — Decisions from custom steps are prefixed with `[CUSTOM]` in markdown/text and styled with a badge in HTML.
|
||||||
|
8. **Detail levels** — `summary` excludes step details. `standard` truncates long outputs. `full` includes everything.
|
||||||
|
9. **Editable preview** — Preview modal allows text editing. Copied content reflects edits. Original session data is unchanged.
|
||||||
|
|
||||||
|
### Phase C Tests
|
||||||
|
|
||||||
|
10. **Redaction** — `redaction_mode=mask` replaces IP addresses, emails, and token patterns with `[REDACTED]` placeholders. `redaction_mode=none` returns original content unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assumptions and Defaults
|
||||||
|
|
||||||
|
- All new `SessionExport` fields have backward-compatible defaults — existing integrations are unaffected.
|
||||||
|
- Mid-session export from `TreeNavigationPage` uses the existing export API endpoint but requires a backend change: only set `exported=True` when session has `completed_at`.
|
||||||
|
- The `next_steps` field is a new dedicated column on the session model (not piggyback on scratchpad or outcome_notes).
|
||||||
|
- Initial scope covers the session detail export flow and the active navigation page. Bulk export or scheduled reports are out of scope.
|
||||||
|
- Redaction (Phase C) starts with conservative regex patterns; false positives are preferred over false negatives.
|
||||||
|
- Export logic lives in `backend/app/services/export_service.py` with four generators (markdown, text, HTML, PSA). The export endpoint in `backend/app/api/endpoints/sessions.py` calls these generators.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. ~~**Outcome notes storage:**~~ **RESOLVED** — `outcome_notes` already exists on the Session model and `SessionComplete` schema. No migration needed.
|
||||||
|
2. ~~**PSA export format:**~~ **RESOLVED** — `generate_psa_export` exists in `export_service.py` and is fully functional. All four formats (markdown, text, HTML, PSA) must receive the same improvements.
|
||||||
|
3. **Step cutoff UX:** Should the step cutoff be a simple number input, a dropdown of step numbers with labels, or a clickable timeline in the session detail view?
|
||||||
|
4. **Redaction scope:** Should hostname redaction be on by default in mask mode, or opt-in? MSP ticket notes often legitimately need hostnames for context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Locked Decisions
|
||||||
|
|
||||||
|
1. **`next_steps` is frozen after completion.** The `update_session` endpoint blocks updates to completed sessions (`sessions.py:190`). `next_steps` follows the same lifecycle — engineers set it during or at completion, not after. If post-completion editing is needed later, a dedicated PATCH endpoint for completion fields can be added as a separate feature.
|
||||||
|
2. **Custom step detection:** The canonical marker is `node_id` starting with `custom-`. No ambiguity — this is what the frontend generates for all custom steps.
|
||||||
|
3. **PSA format included everywhere.** All four generators (markdown, text, HTML, PSA) receive every improvement. The PSA `--- RESOLUTION ---` section uses `outcome_notes` when available, falling back to last decision answer for backward compatibility.
|
||||||
|
4. **Redaction (Phase C) is server-side only.** The editable preview (Phase B4) is a client-side text area — redaction applies to the generated content before it reaches the preview. Edits in the preview are not re-processed.
|
||||||
|
5. **Export audit trail is out of scope.** The boolean `exported` field is sufficient for Phase A. Richer audit (who/when/options) can be a Phase D feature if needed.
|
||||||
|
6. **Selective step inclusion (checkbox/range) is out of scope.** `max_step_index` covers the escalation handoff use case. Non-linear step selection adds significant UX complexity for marginal value.
|
||||||
|
7. **Export presets by PSA destination are out of scope.** The PSA format already targets ConnectWise-style tools. Destination-specific presets can be added when PSA integrations ship (Phase 4 roadmap).
|
||||||
369
docs/plans/2026-02-13-export-phase-a-frontend.md
Normal file
369
docs/plans/2026-02-13-export-phase-a-frontend.md
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# Export Improvements Phase A — Frontend Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Add `next_steps` to types/UI, add "Copy for Ticket" to TreeNavigationPage, add step cutoff control to SessionDetailPage, display next_steps in session detail + completion modal.
|
||||||
|
|
||||||
|
**Architecture:** Update TypeScript types to match backend schema changes. Add `next_steps` textarea to SessionOutcomeModal. Display next_steps on SessionDetailPage. Add mid-session "Copy for Ticket" button to TreeNavigationPage. Add step cutoff dropdown to SessionDetailPage export controls.
|
||||||
|
|
||||||
|
**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand, Vite
|
||||||
|
|
||||||
|
**Backend dependency:** Phase A backend is complete on `feat/export-phase-a` branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Update TypeScript Types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/types/session.ts`
|
||||||
|
|
||||||
|
**Step 1: Add `next_steps` to Session interface** (line 59, after `scratchpad`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
next_steps: string
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Add `next_steps` to SessionUpdate** (line 68, after `scratchpad`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
next_steps?: string
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Add `next_steps` to SessionComplete** (line 83)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface SessionComplete {
|
||||||
|
outcome: SessionOutcome
|
||||||
|
outcome_notes?: string
|
||||||
|
next_steps?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Update SessionExport** (line 77)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface SessionExport {
|
||||||
|
format: 'text' | 'markdown' | 'html' | 'psa'
|
||||||
|
include_timestamps?: boolean
|
||||||
|
include_tree_info?: boolean
|
||||||
|
include_outcome_notes?: boolean
|
||||||
|
include_next_steps?: boolean
|
||||||
|
max_step_index?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Build to verify no type errors**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/types/session.ts
|
||||||
|
git commit -m "feat(frontend): add next_steps and export options to session types"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Add Next Steps to Session Completion Modal
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/components/session/SessionOutcomeModal.tsx`
|
||||||
|
- Modify: `frontend/src/pages/TreeNavigationPage.tsx` (consumer callback)
|
||||||
|
|
||||||
|
**Step 1: Update the `onSubmit` prop type** (line 9)
|
||||||
|
|
||||||
|
Change the data shape to include `next_steps`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
onSubmit: (data: { outcome: SessionOutcome; outcome_notes?: string; next_steps?: string }) => Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Update `handleSubmit`** (line 28)
|
||||||
|
|
||||||
|
Add next_steps extraction from formData:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.current) return
|
||||||
|
const formData = new FormData(formRef.current)
|
||||||
|
const outcome = (formData.get('session-outcome') as SessionOutcome | null) ?? 'resolved'
|
||||||
|
const outcomeNotes = ((formData.get('outcome-notes') as string | null) ?? '').trim()
|
||||||
|
const nextSteps = ((formData.get('next-steps') as string | null) ?? '').trim()
|
||||||
|
|
||||||
|
await onSubmit({
|
||||||
|
outcome,
|
||||||
|
outcome_notes: outcomeNotes || undefined,
|
||||||
|
next_steps: nextSteps || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Add Next Steps textarea** after the outcome notes textarea (after line 115)
|
||||||
|
|
||||||
|
Add a second textarea block right before the closing `</form>`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white">Next Steps / Follow-Up (optional)</label>
|
||||||
|
<textarea
|
||||||
|
name="next-steps"
|
||||||
|
defaultValue=""
|
||||||
|
rows={3}
|
||||||
|
placeholder="Actions to take after this session..."
|
||||||
|
className={cn(
|
||||||
|
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||||
|
'text-sm text-white placeholder:text-white/40',
|
||||||
|
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Update the consumer callback in TreeNavigationPage.tsx** (line 341)
|
||||||
|
|
||||||
|
Change `handleSubmitOutcome` signature to match:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleSubmitOutcome = async (data: { outcome: SessionOutcome; outcome_notes?: string; next_steps?: string }) => {
|
||||||
|
```
|
||||||
|
|
||||||
|
The body already passes `data` directly to `sessionsApi.complete(session.id, data)` so the `next_steps` field will propagate automatically.
|
||||||
|
|
||||||
|
**Step 5: Build to verify**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/components/session/SessionOutcomeModal.tsx frontend/src/pages/TreeNavigationPage.tsx
|
||||||
|
git commit -m "feat(frontend): add next_steps field to session completion modal"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Display Next Steps on SessionDetailPage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/pages/SessionDetailPage.tsx`
|
||||||
|
|
||||||
|
**Step 1: Display next_steps after outcome_notes** (after line 334)
|
||||||
|
|
||||||
|
Find the existing outcome_notes display:
|
||||||
|
```tsx
|
||||||
|
{session.outcome_notes && (
|
||||||
|
<p className="mt-2 text-sm text-white/60">Outcome Notes: {session.outcome_notes}</p>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add next_steps display right after it. Use `whitespace-pre-wrap` to preserve line breaks (engineers often enter bullet lists):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{session.next_steps && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-sm text-white/40">Next Steps:</span>
|
||||||
|
<p className="mt-0.5 text-sm text-white/60 whitespace-pre-wrap">{session.next_steps}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Build to verify**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/pages/SessionDetailPage.tsx
|
||||||
|
git commit -m "feat(frontend): display next_steps on session detail page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Add "Copy for Ticket" Button to TreeNavigationPage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/pages/TreeNavigationPage.tsx`
|
||||||
|
|
||||||
|
This adds a mid-session export button. It should be secondary/outline style — accessible but not prominent (engineers shouldn't think it ends their session).
|
||||||
|
|
||||||
|
**Step 1: Import necessary icons and API**
|
||||||
|
|
||||||
|
Check what's already imported. You'll need `Copy` and `Check` from lucide-react, and `sessionsApi` from `@/api`. Also need `toast` if not already imported.
|
||||||
|
|
||||||
|
**Step 2: Add state for copy feedback and loading** (near other state declarations)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [copiedForTicket, setCopiedForTicket] = useState(false)
|
||||||
|
const [isCopyingForTicket, setIsCopyingForTicket] = useState(false)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Add the copy handler** (with loading guard to prevent double-clicks)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleCopyForTicket = async () => {
|
||||||
|
if (!session || isCopyingForTicket) return
|
||||||
|
setIsCopyingForTicket(true)
|
||||||
|
try {
|
||||||
|
const content = await sessionsApi.export(session.id, {
|
||||||
|
format: 'psa',
|
||||||
|
include_timestamps: true,
|
||||||
|
include_tree_info: true,
|
||||||
|
})
|
||||||
|
if (content) {
|
||||||
|
await navigator.clipboard.writeText(content)
|
||||||
|
setCopiedForTicket(true)
|
||||||
|
setTimeout(() => setCopiedForTicket(false), 2000)
|
||||||
|
toast.success('Copied progress notes to clipboard')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Copy for ticket failed:', err)
|
||||||
|
toast.error('Failed to copy notes')
|
||||||
|
} finally {
|
||||||
|
setIsCopyingForTicket(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Add the button to the page toolbar**
|
||||||
|
|
||||||
|
Find the toolbar/header area of TreeNavigationPage. Look for where other action buttons are (like scratchpad toggle, session timer, etc.). Add a secondary-style button with disabled state:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<button
|
||||||
|
onClick={handleCopyForTicket}
|
||||||
|
disabled={isCopyingForTicket}
|
||||||
|
title="Copy progress notes for ticket"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 rounded-md border border-white/10 px-3 py-1.5 text-xs font-medium text-white/60',
|
||||||
|
'hover:bg-white/10 hover:text-white transition-colors disabled:opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copiedForTicket ? <Check className="h-3.5 w-3.5 text-emerald-400" /> : <Copy className="h-3.5 w-3.5" />}
|
||||||
|
{copiedForTicket ? 'Copied!' : 'Copy for Ticket'}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Place it near other toolbar actions but not in a position where it could be confused with session completion buttons.
|
||||||
|
|
||||||
|
**Step 5: Build to verify**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/pages/TreeNavigationPage.tsx
|
||||||
|
git commit -m "feat(frontend): add mid-session Copy for Ticket button to navigation page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Add Step Cutoff Control to SessionDetailPage Export
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/src/pages/SessionDetailPage.tsx`
|
||||||
|
|
||||||
|
**Step 1: Add state for step cutoff** (near other export state)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [maxStepIndex, setMaxStepIndex] = useState<number | null>(null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Update `fetchExportContent` to include new options** (line 91)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fetchExportContent = async () => {
|
||||||
|
if (!session) return null
|
||||||
|
const options: SessionExport = {
|
||||||
|
format: exportFormat,
|
||||||
|
include_timestamps: true,
|
||||||
|
include_tree_info: true,
|
||||||
|
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
|
||||||
|
}
|
||||||
|
return await sessionsApi.export(session.id, options)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Update `handleCopyForTicket` similarly** (line 135)
|
||||||
|
|
||||||
|
Add `max_step_index` to PSA export options too:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const options: SessionExport = {
|
||||||
|
format: 'psa',
|
||||||
|
include_timestamps: true,
|
||||||
|
include_tree_info: true,
|
||||||
|
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Add step cutoff dropdown** in the export controls area
|
||||||
|
|
||||||
|
Find the export format `<select>` element (line 368). Add a step cutoff dropdown right after it, but only show it when the session has decisions:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{session.decisions.length > 1 && (
|
||||||
|
<select
|
||||||
|
value={maxStepIndex ?? ''}
|
||||||
|
onChange={(e) => setMaxStepIndex(e.target.value ? Number(e.target.value) : null)}
|
||||||
|
aria-label="Export through step"
|
||||||
|
className={cn(
|
||||||
|
'rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||||
|
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<option value="">All steps</option>
|
||||||
|
{session.decisions.map((_, idx) => (
|
||||||
|
<option key={idx + 1} value={idx + 1}>
|
||||||
|
Through step {idx + 1}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Build to verify**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||||
|
|
||||||
|
**Step 6: Full build**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npm run build`
|
||||||
|
|
||||||
|
**Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/src/pages/SessionDetailPage.tsx
|
||||||
|
git commit -m "feat(frontend): add step cutoff control to export options"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Final Verification
|
||||||
|
|
||||||
|
**Step 1: Full build**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/frontend && npm run build`
|
||||||
|
|
||||||
|
Expected: Build succeeds with no errors.
|
||||||
|
|
||||||
|
**Step 2: Verify git status is clean**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
git log --oneline feat/export-phase-a --not main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Acceptance Checklist (Manual QA)
|
||||||
|
|
||||||
|
After all tasks are complete, verify these scenarios with the running app:
|
||||||
|
|
||||||
|
1. **Completion with next_steps:** Complete a session with both outcome_notes and next_steps filled in. Verify both appear on SessionDetailPage with line breaks preserved.
|
||||||
|
2. **Completion without next_steps:** Complete a session with only outcome. Verify no empty "Next Steps" section appears on detail page or in exports.
|
||||||
|
3. **Mid-session copy:** While navigating a tree, click "Copy for Ticket". Paste the result — it should show "In progress" duration and steps completed so far. Verify the session is NOT marked as exported.
|
||||||
|
4. **Step cutoff:** On a completed session with 5+ steps, use the "Through step 3" dropdown, then Preview. Verify only steps 1-3 appear. Verify "Copy for Ticket" also respects the cutoff.
|
||||||
|
5. **Double-click protection:** Click "Copy for Ticket" rapidly on TreeNavigationPage. Verify only one API call fires (button should disable during loading).
|
||||||
753
docs/plans/2026-02-13-export-phase-a.md
Normal file
753
docs/plans/2026-02-13-export-phase-a.md
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
# Export Improvements Phase A — Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Add outcome_notes, next_steps, mid-session export awareness, and partial step cutoff to all four export formats.
|
||||||
|
|
||||||
|
**Architecture:** Add `next_steps` column via migration. Extend `SessionExport` schema with new options. Update all four generators in `export_service.py` to render Resolution + Next Steps sections and support step slicing. Guard `exported=True` behind completion check.
|
||||||
|
|
||||||
|
**Tech Stack:** Python, FastAPI, SQLAlchemy, Alembic, pytest
|
||||||
|
|
||||||
|
**Spec:** [2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md](2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md) — Phase A only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Add `next_steps` column — Migration + Model
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/models/session.py:56` (after scratchpad)
|
||||||
|
- Create: `backend/alembic/versions/034_add_next_steps_to_sessions.py`
|
||||||
|
|
||||||
|
**Step 1: Add column to Session model**
|
||||||
|
|
||||||
|
In `backend/app/models/session.py`, add after the `scratchpad` column (line 58):
|
||||||
|
|
||||||
|
```python
|
||||||
|
next_steps: Mapped[Optional[str]] = mapped_column(
|
||||||
|
Text, nullable=True, server_default=sa.text("''")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Generate migration**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && python -m alembic revision --autogenerate -m "add next_steps to sessions"`
|
||||||
|
|
||||||
|
Review the generated migration — it should add a single `next_steps` TEXT column with server_default `''`.
|
||||||
|
|
||||||
|
Rename the file to `034_add_next_steps_to_sessions.py` and update the revision ID comment if needed for clarity.
|
||||||
|
|
||||||
|
**Step 3: Run migration**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && python -m alembic upgrade head`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/models/session.py backend/alembic/versions/*next_steps*
|
||||||
|
git commit -m "feat: add next_steps column to sessions table"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Update Schemas — SessionExport, SessionUpdate, SessionResponse, SessionComplete
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/schemas/session.py`
|
||||||
|
|
||||||
|
**Step 1: Update SessionExport** (line 82)
|
||||||
|
|
||||||
|
Replace the current `SessionExport` class with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SessionExport(BaseModel):
|
||||||
|
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
|
||||||
|
include_timestamps: bool = True
|
||||||
|
include_tree_info: bool = True
|
||||||
|
# Phase A
|
||||||
|
include_outcome_notes: bool = True
|
||||||
|
include_next_steps: bool = True
|
||||||
|
max_step_index: Optional[int] = Field(None, ge=1, description="1-based inclusive step cutoff")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Add next_steps to SessionUpdate** (line 46)
|
||||||
|
|
||||||
|
Add to `SessionUpdate`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
next_steps: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Add next_steps to SessionResponse** (line 56)
|
||||||
|
|
||||||
|
Add after `outcome_notes`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
next_steps: str = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the validator to normalize both fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@validator('scratchpad', 'next_steps', pre=True, always=True)
|
||||||
|
def normalize_text_fields(cls, v):
|
||||||
|
return v or ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove the old `normalize_scratchpad` validator.
|
||||||
|
|
||||||
|
**Step 4: Add next_steps to SessionComplete** (line 88)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SessionComplete(BaseModel):
|
||||||
|
outcome: SessionOutcome
|
||||||
|
outcome_notes: Optional[str] = None
|
||||||
|
next_steps: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Run tests to verify no regressions**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q`
|
||||||
|
|
||||||
|
Expected: All existing tests pass (new fields have defaults).
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/schemas/session.py
|
||||||
|
git commit -m "feat: add next_steps and export options to session schemas"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Update Completion Endpoint to Save next_steps
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/api/endpoints/sessions.py:236-238`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
In `backend/tests/test_sessions.py`, add after the existing completion tests:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_complete_session_with_next_steps(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test completing session saves next_steps."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={
|
||||||
|
"outcome": "resolved",
|
||||||
|
"outcome_notes": "Fixed the issue",
|
||||||
|
"next_steps": "Monitor for 48 hours"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["next_steps"] == "Monitor for 48 hours"
|
||||||
|
assert data["outcome_notes"] == "Fixed the issue"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_complete_session_with_next_steps -v`
|
||||||
|
|
||||||
|
Expected: FAIL — `next_steps` not saved (returns `""` because endpoint doesn't set it).
|
||||||
|
|
||||||
|
**Step 3: Update completion endpoint**
|
||||||
|
|
||||||
|
In `backend/app/api/endpoints/sessions.py`, line 238, add after `session.outcome_notes = completion_data.outcome_notes`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
session.next_steps = completion_data.next_steps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_complete_session_with_next_steps -v`
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py
|
||||||
|
git commit -m "feat: save next_steps on session completion"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Update SessionUpdate Endpoint to Save next_steps
|
||||||
|
|
||||||
|
**Note:** `update_session` blocks updates to completed sessions (returns 400). This is intentional — `next_steps` is set during active sessions or at completion time, not after. No changes needed to that guard.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/api/endpoints/sessions.py` (the `update_session` handler)
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_session_next_steps(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test updating next_steps via session update."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
response = await client.put(
|
||||||
|
f"/api/v1/sessions/{session_id}",
|
||||||
|
json={"next_steps": "Schedule follow-up call"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["next_steps"] == "Schedule follow-up call"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test — verify it fails**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_update_session_next_steps -v`
|
||||||
|
|
||||||
|
**Step 3: Update the update_session endpoint**
|
||||||
|
|
||||||
|
Find the `update_session` handler in `sessions.py`. Look for where it applies `SessionUpdate` fields to the session object. Add `next_steps` to that block, following the same pattern as `scratchpad`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if update_data.next_steps is not None:
|
||||||
|
session.next_steps = update_data.next_steps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run test — verify it passes**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_update_session_next_steps -v`
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py
|
||||||
|
git commit -m "feat: allow next_steps update via session update endpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Add outcome_notes + next_steps to Export Generators
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/services/export_service.py`
|
||||||
|
- Test: `backend/tests/test_sessions.py`
|
||||||
|
|
||||||
|
This is the core export change. All four generators need Resolution and Next Steps sections after the steps.
|
||||||
|
|
||||||
|
**Step 1: Write failing tests**
|
||||||
|
|
||||||
|
Add to `test_sessions.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_includes_outcome_notes_in_resolution(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test that outcome_notes appear as Resolution section in exports."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={
|
||||||
|
"outcome": "resolved",
|
||||||
|
"outcome_notes": "Replaced failed DIMM in slot A2",
|
||||||
|
"next_steps": "Monitor for 24 hours"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test markdown
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.text
|
||||||
|
assert "## Resolution" in content
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "## Next Steps" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
# Test text
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "text"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "RESOLUTION" in content
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "NEXT STEPS" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
# Test HTML
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "html"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "Resolution" in content
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "Next Steps" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
# Test PSA
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "psa"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_omits_empty_resolution_and_next_steps(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test that empty outcome_notes/next_steps don't create empty sections."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={"outcome": "resolved"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "## Resolution" not in content
|
||||||
|
assert "## Next Steps" not in content
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_exclude_outcome_notes_flag(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test include_outcome_notes=False suppresses resolution section."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={
|
||||||
|
"outcome": "resolved",
|
||||||
|
"outcome_notes": "Should not appear"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown", "include_outcome_notes": False},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "## Resolution" not in content
|
||||||
|
assert "Should not appear" not in content
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests — verify they fail**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "outcome_notes_in_resolution or omits_empty_resolution or exclude_outcome_notes_flag" -v`
|
||||||
|
|
||||||
|
**Step 3: Update all four generators in export_service.py**
|
||||||
|
|
||||||
|
Add Resolution and Next Steps sections after the Troubleshooting Steps in each generator. The generators receive `options: SessionExport` — use `options.include_outcome_notes` and `options.include_next_steps`.
|
||||||
|
|
||||||
|
**Markdown generator** — add before `return "\n".join(lines)` (line 178):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Resolution / Outcome Notes
|
||||||
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Resolution")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(outcome_notes.strip())
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
next_steps = getattr(session, 'next_steps', '') or ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Next Steps")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(next_steps.strip())
|
||||||
|
lines.append("")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Text generator** — add before `return "\n".join(lines)` (line 235):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Resolution
|
||||||
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("RESOLUTION")
|
||||||
|
lines.append("-" * 20)
|
||||||
|
lines.append(outcome_notes.strip())
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
next_steps = getattr(session, 'next_steps', '') or ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("NEXT STEPS")
|
||||||
|
lines.append("-" * 20)
|
||||||
|
lines.append(next_steps.strip())
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTML generator** — add before `html_parts.extend(['</body>', '</html>'])` (line 307):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Resolution
|
||||||
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
html_parts.append('<h2>Resolution</h2>')
|
||||||
|
html_parts.append(f'<div style="white-space: pre-wrap; margin-bottom: 20px;">{html.escape(outcome_notes.strip())}</div>')
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
next_steps = getattr(session, 'next_steps', '') or ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
html_parts.append('<h2>Next Steps</h2>')
|
||||||
|
html_parts.append(f'<div style="white-space: pre-wrap; margin-bottom: 20px;">{html.escape(next_steps.strip())}</div>')
|
||||||
|
```
|
||||||
|
|
||||||
|
**PSA generator** — update the existing `--- RESOLUTION ---` section (lines 363-373) to use `outcome_notes` when available, and add a next steps section:
|
||||||
|
|
||||||
|
Replace the resolution section with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Resolution
|
||||||
|
lines.append("--- RESOLUTION ---")
|
||||||
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
lines.append(outcome_notes.strip())
|
||||||
|
elif session.decisions:
|
||||||
|
last_decision = session.decisions[-1]
|
||||||
|
resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded")
|
||||||
|
lines.append(resolution)
|
||||||
|
else:
|
||||||
|
lines.append("No resolution recorded.")
|
||||||
|
if outcome_label:
|
||||||
|
lines.append(f"Outcome: {outcome_label}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
next_steps = getattr(session, 'next_steps', '') or ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
lines.append("--- NEXT STEPS ---")
|
||||||
|
lines.append(next_steps.strip())
|
||||||
|
lines.append("")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run tests — verify they pass**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "outcome_notes_in_resolution or omits_empty_resolution or exclude_outcome_notes_flag" -v`
|
||||||
|
|
||||||
|
**Step 5: Run full test suite**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q`
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/export_service.py backend/tests/test_sessions.py
|
||||||
|
git commit -m "feat: add Resolution and Next Steps sections to all export formats"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Partial Export with Step Cutoff (max_step_index)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/services/export_service.py`
|
||||||
|
- Test: `backend/tests/test_sessions.py`
|
||||||
|
|
||||||
|
**Step 1: Write failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_max_step_index(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test max_step_index limits exported steps."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
# Add 3 decisions
|
||||||
|
decisions = [
|
||||||
|
{"node_id": "n1", "question": "Step one?", "answer": "Yes", "timestamp": "2026-02-13T10:00:00Z", "attachments": []},
|
||||||
|
{"node_id": "n2", "question": "Step two?", "answer": "No", "timestamp": "2026-02-13T10:01:00Z", "attachments": []},
|
||||||
|
{"node_id": "n3", "question": "Step three?", "answer": "Maybe", "timestamp": "2026-02-13T10:02:00Z", "attachments": []},
|
||||||
|
]
|
||||||
|
await client.put(
|
||||||
|
f"/api/v1/sessions/{session_id}",
|
||||||
|
json={"decisions": decisions},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Export with cutoff at step 2
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown", "max_step_index": 2},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "Step one?" in content
|
||||||
|
assert "Step two?" in content
|
||||||
|
assert "Step three?" not in content
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_max_step_index_exceeds_count(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test max_step_index larger than decision count returns all steps."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
decisions = [
|
||||||
|
{"node_id": "n1", "question": "Only step", "answer": "Done", "timestamp": "2026-02-13T10:00:00Z", "attachments": []},
|
||||||
|
]
|
||||||
|
await client.put(
|
||||||
|
f"/api/v1/sessions/{session_id}",
|
||||||
|
json={"decisions": decisions},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown", "max_step_index": 100},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Only step" in response.text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_max_step_index_zero_returns_422(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test max_step_index=0 returns validation error."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown", "max_step_index": 0},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests — verify they fail** (the 422 test should already pass due to `ge=1` on schema)
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "max_step_index" -v`
|
||||||
|
|
||||||
|
**Step 3: Apply step slicing in all four generators**
|
||||||
|
|
||||||
|
In each generator, right before the `for i, decision in enumerate(session.decisions, 1):` loop, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
decisions = session.decisions
|
||||||
|
if options.max_step_index is not None:
|
||||||
|
decisions = decisions[:options.max_step_index]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then change the loop to iterate over `decisions` instead of `session.decisions`.
|
||||||
|
|
||||||
|
Apply this in:
|
||||||
|
- `generate_markdown_export` (line ~153)
|
||||||
|
- `generate_text_export` (line ~214)
|
||||||
|
- `generate_html_export` (line ~283)
|
||||||
|
- `generate_psa_export` (line ~338)
|
||||||
|
|
||||||
|
**Step 4: Run tests — verify they pass**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "max_step_index" -v`
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/export_service.py backend/tests/test_sessions.py
|
||||||
|
git commit -m "feat: add max_step_index for partial exports"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Mid-Session Export — Don't Set exported=True
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/api/endpoints/sessions.py:316-318`
|
||||||
|
- Test: `backend/tests/test_sessions.py`
|
||||||
|
|
||||||
|
**Step 1: Write failing test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_in_progress_session_does_not_mark_exported(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test that exporting an in-progress session does NOT set exported=True."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
# Export without completing
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check session - should NOT be marked exported
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/v1/sessions/{session_id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.json()["exported"] is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_completed_session_marks_exported(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test that exporting a completed session sets exported=True."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
# Complete first
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={"outcome": "resolved"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Export
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check - should be marked exported
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/v1/sessions/{session_id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.json()["exported"] is True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests — verify the first one fails**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "in_progress_session_does_not_mark_exported or completed_session_marks_exported" -v`
|
||||||
|
|
||||||
|
**Step 3: Update export endpoint**
|
||||||
|
|
||||||
|
In `backend/app/api/endpoints/sessions.py`, replace lines 316-318:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Mark as exported
|
||||||
|
session.exported = True
|
||||||
|
await db.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Only mark as exported if session is completed
|
||||||
|
if session.completed_at:
|
||||||
|
session.exported = True
|
||||||
|
await db.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run tests — verify they pass**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "in_progress_session_does_not_mark_exported or completed_session_marks_exported" -v`
|
||||||
|
|
||||||
|
**Step 5: Run full test suite**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q`
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py
|
||||||
|
git commit -m "feat: only mark session exported when completed"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Final Verification
|
||||||
|
|
||||||
|
**Step 1: Run full test suite**
|
||||||
|
|
||||||
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -v`
|
||||||
|
|
||||||
|
Expected: All tests pass, including all new tests.
|
||||||
|
|
||||||
|
**Step 2: Verify backward compatibility**
|
||||||
|
|
||||||
|
Confirm that the existing export tests (the ones from before our changes) still pass with no modifications — the new schema fields all have defaults.
|
||||||
|
|
||||||
|
**Step 3: Commit any remaining changes and verify git status is clean**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
@@ -1,528 +0,0 @@
|
|||||||
# ResolutionFlow Design System Implementation Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This guide provides everything needed to implement the new monochrome design with subtle icon accents across the entire ResolutionFlow application.
|
|
||||||
|
|
||||||
## Design Philosophy
|
|
||||||
- **95% Monochrome**: Pure black backgrounds, white text, transparent card overlays
|
|
||||||
- **5% Color**: Subtle colors ONLY on functional icons
|
|
||||||
- **Inspiration**: Plasma + Aspect templates (minimalist, modern, enterprise-grade)
|
|
||||||
- **Key Principle**: Restraint and sophistication over flashy gradients
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Color Palette
|
|
||||||
|
|
||||||
### Background Colors
|
|
||||||
```css
|
|
||||||
/* Main background - pure black with subtle gradient */
|
|
||||||
background: linear-gradient(to bottom, #000000 0%, #0a0a0a 50%, #000000 100%);
|
|
||||||
|
|
||||||
/* Subtle radial overlays (optional, adds depth) */
|
|
||||||
background: radial-gradient(circle at 50% 0%, rgba(100, 100, 120, 0.03), transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 80%, rgba(80, 80, 100, 0.02), transparent 50%);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Text Colors
|
|
||||||
```css
|
|
||||||
/* Primary text */
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
/* Secondary text */
|
|
||||||
color: rgba(255, 255, 255, 0.7); /* white/70 */
|
|
||||||
|
|
||||||
/* Tertiary text */
|
|
||||||
color: rgba(255, 255, 255, 0.4); /* white/40 */
|
|
||||||
|
|
||||||
/* Subtle text */
|
|
||||||
color: rgba(255, 255, 255, 0.3); /* white/30 */
|
|
||||||
```
|
|
||||||
|
|
||||||
### Card/Surface Colors
|
|
||||||
```css
|
|
||||||
/* Standard cards */
|
|
||||||
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
|
|
||||||
/* Card hover state */
|
|
||||||
background: linear-gradient(135deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%);
|
|
||||||
border-color: rgba(255, 255, 255, 0.12);
|
|
||||||
|
|
||||||
/* Stat cards */
|
|
||||||
background: rgba(20, 20, 25, 0.5);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
|
|
||||||
/* Active/Highlighted card (Bright Glow) */
|
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
box-shadow: 0 0 40px rgba(255, 255, 255, 0.1);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Icon Colors (THE ONLY COLORS IN THE APP)
|
|
||||||
```css
|
|
||||||
/* AI/Sparkle icons */
|
|
||||||
color: #22d3ee; /* cyan-400 */
|
|
||||||
|
|
||||||
/* Search icons */
|
|
||||||
color: #60a5fa; /* blue-400 */
|
|
||||||
|
|
||||||
/* Active/Play icons */
|
|
||||||
color: #a78bfa; /* violet-400 */
|
|
||||||
|
|
||||||
/* Network category */
|
|
||||||
color: #60a5fa; /* blue-400 */
|
|
||||||
|
|
||||||
/* Printer category */
|
|
||||||
color: #818cf8; /* indigo-400 */
|
|
||||||
|
|
||||||
/* Email category */
|
|
||||||
color: #22d3ee; /* cyan-400 */
|
|
||||||
|
|
||||||
/* Success/Up trends */
|
|
||||||
color: #34d399; /* emerald-400 */
|
|
||||||
|
|
||||||
/* Failure/Down trends */
|
|
||||||
color: #f87171; /* red-400 */
|
|
||||||
|
|
||||||
/* Neutral/Time icons (NO COLOR) */
|
|
||||||
color: rgba(255, 255, 255, 0.5); /* gray */
|
|
||||||
```
|
|
||||||
|
|
||||||
### Button Colors
|
|
||||||
```css
|
|
||||||
/* Primary button (white) */
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
/* Primary button hover */
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
|
|
||||||
/* Secondary button */
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
/* Secondary button hover */
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Typography
|
|
||||||
|
|
||||||
### Font Family
|
|
||||||
```css
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Font Weights
|
|
||||||
- Regular: 400
|
|
||||||
- Medium: 500
|
|
||||||
- Semibold: 600
|
|
||||||
- Bold: 700
|
|
||||||
- Extrabold: 800
|
|
||||||
|
|
||||||
### Type Scale
|
|
||||||
```css
|
|
||||||
/* Hero heading */
|
|
||||||
font-size: 3.5rem; /* 56px */
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.1;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
|
|
||||||
/* Section heading */
|
|
||||||
font-size: 2rem; /* 32px */
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
|
|
||||||
/* Card title */
|
|
||||||
font-size: 1.25rem; /* 20px */
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
|
|
||||||
/* Body large */
|
|
||||||
font-size: 1.25rem; /* 20px */
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.6;
|
|
||||||
|
|
||||||
/* Body */
|
|
||||||
font-size: 1rem; /* 16px */
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
/* Small */
|
|
||||||
font-size: 0.875rem; /* 14px */
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.4;
|
|
||||||
|
|
||||||
/* Extra small */
|
|
||||||
font-size: 0.75rem; /* 12px */
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.3;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Component Patterns
|
|
||||||
|
|
||||||
### Header/Navigation
|
|
||||||
```jsx
|
|
||||||
<header className="mb-16">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* White logo icon */}
|
|
||||||
<div className="w-9 h-9 rounded-xl bg-white flex items-center justify-center">
|
|
||||||
<svg className="w-5 h-5 text-black" /* icon */></svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-xl font-semibold text-white tracking-tight">
|
|
||||||
ResolutionFlow
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="px-4 py-2 rounded-xl bg-white/10 border border-white/20">
|
|
||||||
<span className="text-sm text-white font-semibold">Admin</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hero Section
|
|
||||||
```jsx
|
|
||||||
<div className="mb-20 text-center max-w-4xl mx-auto">
|
|
||||||
{/* Badge with colored icon */}
|
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
|
|
||||||
<svg className="w-4 h-4 text-cyan-400" /* sparkle icon */></svg>
|
|
||||||
<span className="text-sm text-white/70 font-medium">AI-POWERED TROUBLESHOOTING</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main heading */}
|
|
||||||
<h1 className="text-5xl md:text-7xl font-bold text-white mb-6 tracking-tight leading-tight">
|
|
||||||
All Your Tickets in One<br>
|
|
||||||
<span className="text-white/60">Unified Dashboard</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-xl text-white/40 mb-12 max-w-2xl mx-auto leading-relaxed">
|
|
||||||
Search our library of proven decision trees or continue where you left off
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Search Bar
|
|
||||||
```jsx
|
|
||||||
<div className="relative max-w-2xl mx-auto group">
|
|
||||||
<div className="absolute inset-0 bg-white/5 rounded-2xl blur-2xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
||||||
<div className="relative bg-gradient-to-br from-white/[0.04] to-white/[0.01] border border-white/8 backdrop-blur-xl rounded-2xl p-1">
|
|
||||||
<div className="flex items-center bg-black/50 rounded-xl">
|
|
||||||
{/* Blue search icon */}
|
|
||||||
<svg className="ml-5 w-5 h-5 text-blue-400" /* search icon */></svg>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Paste ticket subject or search for a tree..."
|
|
||||||
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
|
|
||||||
/>
|
|
||||||
{/* White button */}
|
|
||||||
<button className="mr-2 px-5 py-2.5 bg-white text-black font-semibold rounded-lg hover:bg-white/90 transition-all">
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stat Card
|
|
||||||
```jsx
|
|
||||||
<div className="bg-[rgba(20,20,25,0.5)] border border-white/[0.06] backdrop-blur-xl rounded-2xl p-6 hover:scale-105 transition-transform">
|
|
||||||
<div className="text-sm text-white/40 mb-2 font-medium">Active Sessions</div>
|
|
||||||
<div className="text-4xl font-bold text-white mb-1">18</div>
|
|
||||||
<div className="flex items-center gap-1 text-xs text-emerald-400 font-medium">
|
|
||||||
{/* Green up arrow icon */}
|
|
||||||
<svg className="w-3 h-3" /* arrow up icon */></svg>
|
|
||||||
12% vs last week
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Active Session Card (Bright Glow)
|
|
||||||
```jsx
|
|
||||||
<div className="bg-gradient-to-br from-white/[0.08] to-white/[0.04] border border-white/20 backdrop-blur-xl rounded-2xl p-8 shadow-[0_0_40px_rgba(255,255,255,0.1)]">
|
|
||||||
<div className="flex items-start justify-between mb-6">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
{/* Icon with violet color */}
|
|
||||||
<div className="w-12 h-12 rounded-xl bg-white/15 border border-white/30 flex items-center justify-center">
|
|
||||||
<svg className="w-6 h-6 text-violet-400" /* play icon */></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-white/70 font-semibold uppercase tracking-wider mb-1">
|
|
||||||
Active Session
|
|
||||||
</div>
|
|
||||||
<h3 className="text-2xl font-bold text-white">Email Delivery Issues</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-white/50">Ticket #5847 • Started 2h ago • 11 of 15 steps</p>
|
|
||||||
</div>
|
|
||||||
<button className="px-4 py-2 bg-white text-black rounded-xl font-semibold hover:bg-white/90 transition-all">
|
|
||||||
Continue →
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between text-sm text-white/70 mb-3 font-medium">
|
|
||||||
<span>Progress</span>
|
|
||||||
<span>72%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
|
|
||||||
<div className="h-full bg-white rounded-full" style={{width: '72%'}}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tree Card
|
|
||||||
```jsx
|
|
||||||
<div className="bg-gradient-to-br from-white/[0.04] to-white/[0.01] border border-white/8 backdrop-blur-xl rounded-2xl p-6 hover:scale-105 hover:from-white/[0.06] hover:to-white/[0.02] hover:border-white/12 transition-all cursor-pointer">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
{/* Icon with category-specific color */}
|
|
||||||
<div className="w-12 h-12 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
|
||||||
<svg className="w-6 h-6 text-blue-400" /* network icon */></svg>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 py-1 rounded-lg bg-white/10 border border-white/20">
|
|
||||||
<span className="text-xs text-white/80 font-semibold">Available</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-bold text-white mb-2">Network Connectivity</h4>
|
|
||||||
<p className="text-sm text-white/40 mb-4">Diagnose and resolve network issues</p>
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-white/30">
|
|
||||||
{/* Gray clock icon (NO COLOR) */}
|
|
||||||
<svg className="w-3.5 h-3.5 text-slate-500" /* clock icon */></svg>
|
|
||||||
Last used 5h ago
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Primary Button (White)
|
|
||||||
```jsx
|
|
||||||
<button className="px-8 py-4 bg-white text-black font-bold rounded-xl hover:bg-white/90 transition-all hover:scale-105">
|
|
||||||
Get Started Free
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Secondary Button (Transparent)
|
|
||||||
```jsx
|
|
||||||
<button className="px-8 py-4 bg-white/10 border border-white/20 text-white font-semibold rounded-xl hover:bg-white/20 transition-all">
|
|
||||||
Book a Demo
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Icon Color Mapping
|
|
||||||
|
|
||||||
### Where to use each color:
|
|
||||||
|
|
||||||
**Cyan (#22d3ee / cyan-400):**
|
|
||||||
- AI/Sparkle icons
|
|
||||||
- Email-related icons
|
|
||||||
- Magic/automation indicators
|
|
||||||
|
|
||||||
**Blue (#60a5fa / blue-400):**
|
|
||||||
- Search icons
|
|
||||||
- Network-related icons
|
|
||||||
- General tech icons
|
|
||||||
|
|
||||||
**Violet (#a78bfa / violet-400):**
|
|
||||||
- Active/playing state icons
|
|
||||||
- Current session indicators
|
|
||||||
- Progress-related icons
|
|
||||||
|
|
||||||
**Indigo (#818cf8 / indigo-400):**
|
|
||||||
- Printer-related icons
|
|
||||||
- Hardware icons
|
|
||||||
|
|
||||||
**Emerald (#34d399 / emerald-400):**
|
|
||||||
- Success indicators
|
|
||||||
- Up arrows/trends
|
|
||||||
- Positive metrics
|
|
||||||
|
|
||||||
**Red (#f87171 / red-400):**
|
|
||||||
- Error indicators
|
|
||||||
- Down arrows/trends
|
|
||||||
- Negative metrics
|
|
||||||
|
|
||||||
**Gray (rgba(255,255,255,0.5) / white/50):**
|
|
||||||
- Time/clock icons
|
|
||||||
- Neutral informational icons
|
|
||||||
- Non-critical icons
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tailwind Classes Reference
|
|
||||||
|
|
||||||
### Background Patterns
|
|
||||||
```
|
|
||||||
bg-black
|
|
||||||
bg-white
|
|
||||||
bg-white/5 (5% opacity)
|
|
||||||
bg-white/10 (10% opacity)
|
|
||||||
bg-white/15 (15% opacity)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Text Opacity
|
|
||||||
```
|
|
||||||
text-white
|
|
||||||
text-white/70
|
|
||||||
text-white/60
|
|
||||||
text-white/50
|
|
||||||
text-white/40
|
|
||||||
text-white/30
|
|
||||||
```
|
|
||||||
|
|
||||||
### Border Opacity
|
|
||||||
```
|
|
||||||
border-white/8
|
|
||||||
border-white/10
|
|
||||||
border-white/12
|
|
||||||
border-white/20
|
|
||||||
border-white/30
|
|
||||||
```
|
|
||||||
|
|
||||||
### Icon Colors
|
|
||||||
```
|
|
||||||
text-cyan-400
|
|
||||||
text-blue-400
|
|
||||||
text-violet-400
|
|
||||||
text-indigo-400
|
|
||||||
text-emerald-400
|
|
||||||
text-red-400
|
|
||||||
text-slate-500
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rounded Corners
|
|
||||||
```
|
|
||||||
rounded-xl (12px - cards, buttons)
|
|
||||||
rounded-2xl (16px - large cards)
|
|
||||||
rounded-3xl (24px - hero sections)
|
|
||||||
rounded-full (perfect circle - badges, dots)
|
|
||||||
rounded-lg (8px - small elements)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Spacing
|
|
||||||
```
|
|
||||||
gap-1, gap-2, gap-3, gap-4, gap-6, gap-8
|
|
||||||
p-4, p-6, p-8, p-12
|
|
||||||
mb-2, mb-3, mb-4, mb-6, mb-8, mb-12, mb-16, mb-20
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Animation & Transitions
|
|
||||||
|
|
||||||
### Standard Transition
|
|
||||||
```css
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hover Scale
|
|
||||||
```css
|
|
||||||
hover:scale-105
|
|
||||||
transition-transform
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hover Opacity
|
|
||||||
```css
|
|
||||||
hover:opacity-100
|
|
||||||
transition-opacity
|
|
||||||
```
|
|
||||||
|
|
||||||
### Card Hover Glow
|
|
||||||
```css
|
|
||||||
hover:shadow-[0_0_40px_rgba(255,255,255,0.15)]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Checklist
|
|
||||||
|
|
||||||
### Phase 1: Global Styles
|
|
||||||
- [ ] Update background color to pure black gradient
|
|
||||||
- [ ] Set font-family to Inter
|
|
||||||
- [ ] Add subtle radial gradient overlays
|
|
||||||
|
|
||||||
### Phase 2: Component Updates
|
|
||||||
- [ ] Header/Navigation
|
|
||||||
- [ ] Hero sections
|
|
||||||
- [ ] Search bars
|
|
||||||
- [ ] Stat cards
|
|
||||||
- [ ] Active session cards (Bright Glow)
|
|
||||||
- [ ] Tree/item cards
|
|
||||||
- [ ] Buttons (primary & secondary)
|
|
||||||
- [ ] Forms/inputs
|
|
||||||
- [ ] Modals
|
|
||||||
- [ ] Empty states
|
|
||||||
|
|
||||||
### Phase 3: Icon Colors
|
|
||||||
- [ ] Map all icons to appropriate colors
|
|
||||||
- [ ] Keep time/clock icons gray
|
|
||||||
- [ ] Add category colors to tree icons
|
|
||||||
|
|
||||||
### Phase 4: Polish
|
|
||||||
- [ ] Add hover states to all interactive elements
|
|
||||||
- [ ] Test backdrop-filter blur support
|
|
||||||
- [ ] Verify contrast ratios for accessibility
|
|
||||||
- [ ] Test on dark displays
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes for Claude Code
|
|
||||||
|
|
||||||
1. **Preserve existing functionality** - Only change visual styling, not logic
|
|
||||||
2. **Use Tailwind classes** - Avoid custom CSS where possible
|
|
||||||
3. **Keep shadcn/ui components** - Just reskin them with new colors
|
|
||||||
4. **Test incrementally** - Update one component type at a time
|
|
||||||
5. **Icon mapping is critical** - Different tree types get different icon colors
|
|
||||||
|
|
||||||
## Questions to Resolve
|
|
||||||
|
|
||||||
1. Should all buttons use the same white style, or do you want variations?
|
|
||||||
2. Do you want the subtle radial gradient overlays, or pure black background?
|
|
||||||
3. Should stat cards always show trend indicators (up/down arrows)?
|
|
||||||
4. Any specific components that should NOT follow this design?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Example Files to Update
|
|
||||||
|
|
||||||
**Priority 1 (Core UI):**
|
|
||||||
- `AppLayout.tsx` - Main layout wrapper
|
|
||||||
- `QuickStartPage.tsx` - Homepage/dashboard
|
|
||||||
- `Header.tsx` / `Navbar.tsx` - Navigation
|
|
||||||
|
|
||||||
**Priority 2 (Features):**
|
|
||||||
- Tree list components
|
|
||||||
- Session components
|
|
||||||
- Search components
|
|
||||||
- Stats/metrics components
|
|
||||||
|
|
||||||
**Priority 3 (Supporting):**
|
|
||||||
- Modals
|
|
||||||
- Forms
|
|
||||||
- Settings pages
|
|
||||||
- Admin panels
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
After implementation:
|
|
||||||
- [ ] Dark mode looks correct
|
|
||||||
- [ ] All text is readable (contrast check)
|
|
||||||
- [ ] Icons have correct colors
|
|
||||||
- [ ] Hover states work
|
|
||||||
- [ ] Cards have proper depth/hierarchy
|
|
||||||
- [ ] Buttons are prominent
|
|
||||||
- [ ] Layout is consistent across pages
|
|
||||||
581
docs/plans/ai-guided-flow-creation.md
Normal file
581
docs/plans/ai-guided-flow-creation.md
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
# AI-Guided Flow Creation — Design Document
|
||||||
|
|
||||||
|
> **Date:** 2026-02-12
|
||||||
|
> **Status:** Draft
|
||||||
|
> **Phase:** 3 (AI Intelligence)
|
||||||
|
> **Dependencies:** Global Categories, Flow Editor, Plan Limits, Session Tracking
|
||||||
|
> **Estimated Effort:** 3-4 weeks (backend + frontend + prompt engineering)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
AI-Guided Flow Creation is an interactive wizard that helps engineers build troubleshooting and procedural flows through a structured conversation. Rather than generating a complete flow from a single description (expensive, low-quality, black box), the wizard asks targeted questions at each stage, using the engineer's domain expertise to shape the output while AI fills in the details.
|
||||||
|
|
||||||
|
**Core Principle:** The engineer knows the problem domain. The AI knows how to structure it. The wizard brings both together.
|
||||||
|
|
||||||
|
**Key Differentiator:** This is NOT a "describe your problem and AI builds everything" feature. It's a collaborative creation tool where AI assists at specific, bounded points — keeping costs predictable, output quality high, and the engineer in control.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Approach
|
||||||
|
|
||||||
|
| Approach | Cost/Flow | Quality | Engineer Control | Reusability |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Full AI generation (one-shot) | ~$0.03 | Medium (needs heavy editing) | None until review | None |
|
||||||
|
| **Guided wizard (this design)** | **~$0.01-0.02** | **High (shaped by engineer)** | **At every step** | **High (cached prompts)** |
|
||||||
|
| Manual creation only | $0.00 | Varies | Full | None |
|
||||||
|
|
||||||
|
The guided approach wins on every axis because:
|
||||||
|
|
||||||
|
1. **Smaller, targeted API calls** replace one large generation call
|
||||||
|
2. **Fixed question patterns** enable aggressive prompt caching (90% input savings)
|
||||||
|
3. **Engineer input at each stage** means less post-generation editing
|
||||||
|
4. **Structured data collection** before any AI call means better prompts and better output
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Flow
|
||||||
|
|
||||||
|
### Stage 1: Foundation (No AI — Pure UI)
|
||||||
|
|
||||||
|
The user provides structured metadata that will inform all subsequent AI calls.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ CREATE A NEW FLOW │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Flow Type: │
|
||||||
|
│ ○ Troubleshooting (diagnostic branching — "what's wrong?")│
|
||||||
|
│ ○ Procedure (step-by-step — "how do I set this up?") │
|
||||||
|
│ │
|
||||||
|
│ Category: │
|
||||||
|
│ [▼ Networking ] │
|
||||||
|
│ │
|
||||||
|
│ Flow Name: │
|
||||||
|
│ [Printer Not Printing ] │
|
||||||
|
│ │
|
||||||
|
│ Brief Description: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ User reports print jobs stuck in queue, printer │ │
|
||||||
|
│ │ shows as offline or errors when printing │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Target Environment (optional): │
|
||||||
|
│ □ Windows □ macOS □ Linux │
|
||||||
|
│ □ Cloud/SaaS □ On-Premises □ Hybrid │
|
||||||
|
│ │
|
||||||
|
│ [Next →] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data collected:** flow_type, category_id, name, description, environment tags.
|
||||||
|
**AI cost:** $0.00
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stage 2: Structure Scaffolding (Light AI)
|
||||||
|
|
||||||
|
Based on Stage 1 input, AI suggests the top-level structure. The user confirms, removes, or adds items.
|
||||||
|
|
||||||
|
**For Troubleshooting flows — AI suggests initial symptom branches:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ WHAT SYMPTOMS WOULD A USER REPORT? │
|
||||||
|
│ │
|
||||||
|
│ Based on "Printer Not Printing", here are common starting │
|
||||||
|
│ points. Check the ones that apply, add your own: │
|
||||||
|
│ │
|
||||||
|
│ AI Suggestions: │
|
||||||
|
│ ☑ Print jobs stuck in queue / never print │
|
||||||
|
│ ☑ Printer shows offline │
|
||||||
|
│ ☑ Prints but output is garbled or wrong │
|
||||||
|
│ ☐ Printer not found / can't add printer │
|
||||||
|
│ ☑ Specific application can't print (others can) │
|
||||||
|
│ │
|
||||||
|
│ + Add your own symptom: [____________________________] │
|
||||||
|
│ │
|
||||||
|
│ [← Back] [Next →] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Procedure flows — AI suggests major phases:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ WHAT ARE THE MAJOR PHASES? │
|
||||||
|
│ │
|
||||||
|
│ Based on "New Server Build", here are typical phases. │
|
||||||
|
│ Check the ones that apply, reorder, add your own: │
|
||||||
|
│ │
|
||||||
|
│ AI Suggestions: │
|
||||||
|
│ ☑ 1. Pre-requisites & Planning │
|
||||||
|
│ ☑ 2. OS Installation & Base Config │
|
||||||
|
│ ☑ 3. Network Configuration │
|
||||||
|
│ ☑ 4. Domain Join & Security │
|
||||||
|
│ ☑ 5. Role/Application Installation │
|
||||||
|
│ ☑ 6. Verification & Handoff │
|
||||||
|
│ │
|
||||||
|
│ + Add a phase: [____________________________] │
|
||||||
|
│ │
|
||||||
|
│ [← Back] [Next →] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**API call:** 1 call to Haiku 4.5. Input: system prompt (cached) + stage 1 metadata. Output: 5-8 suggestions as JSON array.
|
||||||
|
**Estimated cost:** ~$0.003-0.005
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stage 3: Branch/Step Detail (Light AI, per branch)
|
||||||
|
|
||||||
|
For each item the user selected in Stage 2, AI suggests the diagnostic steps or sub-steps. The user processes one branch/phase at a time.
|
||||||
|
|
||||||
|
**For Troubleshooting — diagnostic steps per symptom:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ BRANCH: "Print jobs stuck in queue" │
|
||||||
|
│ │
|
||||||
|
│ What steps should an engineer follow for this symptom? │
|
||||||
|
│ │
|
||||||
|
│ AI Suggestions: │
|
||||||
|
│ ☑ Check Print Spooler service status │
|
||||||
|
│ Action: Run Get-Service Spooler — restart if stopped │
|
||||||
|
│ │
|
||||||
|
│ ☑ Clear the print queue │
|
||||||
|
│ Action: Stop spooler, delete files in │
|
||||||
|
│ C:\Windows\System32\spool\PRINTERS, restart spooler │
|
||||||
|
│ │
|
||||||
|
│ ☑ Verify printer port and driver │
|
||||||
|
│ Decision: Is the printer networked or USB? │
|
||||||
|
│ → Networked: ping printer IP, check port config │
|
||||||
|
│ → USB: check cable, try different port │
|
||||||
|
│ │
|
||||||
|
│ ☑ Test with different application │
|
||||||
|
│ Decision: Does it print from Notepad? │
|
||||||
|
│ → Yes: Application-specific issue │
|
||||||
|
│ → No: System-level print problem │
|
||||||
|
│ │
|
||||||
|
│ + Add your own step: [____________________________] │
|
||||||
|
│ │
|
||||||
|
│ [✎ Edit any step] [← Back] [Next →] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Procedure — detailed steps per phase:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PHASE: "OS Installation & Base Config" │
|
||||||
|
│ │
|
||||||
|
│ What steps should be performed in this phase? │
|
||||||
|
│ │
|
||||||
|
│ AI Suggestions: │
|
||||||
|
│ ☑ Boot from installation media │
|
||||||
|
│ Action: Mount ISO / insert USB, boot to BIOS, set │
|
||||||
|
│ boot order │
|
||||||
|
│ │
|
||||||
|
│ ☑ Configure disk partitions │
|
||||||
|
│ Action: Create partitions per standard │
|
||||||
|
│ 📝 Record: "Partition layout used" │
|
||||||
|
│ │
|
||||||
|
│ ☑ Set hostname │
|
||||||
|
│ Action: Follow naming convention: SITE-ROLE-## │
|
||||||
|
│ 📝 Record: "Hostname assigned" │
|
||||||
|
│ │
|
||||||
|
│ ☑ Configure local admin account │
|
||||||
|
│ Action: Set password per policy, disable built-in │
|
||||||
|
│ Administrator │
|
||||||
|
│ │
|
||||||
|
│ ☑ Install Windows Updates │
|
||||||
|
│ Action: Run full update cycle, reboot, repeat until │
|
||||||
|
│ clean │
|
||||||
|
│ │
|
||||||
|
│ + Add your own step: [____________________________] │
|
||||||
|
│ │
|
||||||
|
│ [← Back] [Next →] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note the 📝 Record fields** — for procedure flows, AI can suggest which steps should capture data (hostname, IP, etc.). This feeds directly into the session documentation.
|
||||||
|
|
||||||
|
**API call:** 1 call per branch/phase. Input: system prompt (cached) + stage 1 metadata + branch name + context from other branches. Output: 3-6 detailed steps as JSON.
|
||||||
|
**Estimated cost:** ~$0.003-0.005 per branch. 5 branches = ~$0.015-0.025
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stage 4: Review & Refine (No AI)
|
||||||
|
|
||||||
|
The complete flow is assembled and shown in the existing tree/flow editor. User can:
|
||||||
|
|
||||||
|
- Rearrange nodes
|
||||||
|
- Edit text on any step
|
||||||
|
- Add/remove branches
|
||||||
|
- Preview the flow as an end-user would see it
|
||||||
|
- Save as draft or publish
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ REVIEW YOUR FLOW │
|
||||||
|
│ │
|
||||||
|
│ "Printer Not Printing" — Troubleshooting Flow │
|
||||||
|
│ Category: Networking | 5 branches | 23 steps │
|
||||||
|
│ │
|
||||||
|
│ [Visual tree/flow editor — existing component] │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Root: What is the printing issue? │
|
||||||
|
│ ├── Jobs stuck in queue (5 steps) │
|
||||||
|
│ ├── Printer shows offline (4 steps) │
|
||||||
|
│ ├── Garbled output (3 steps) │
|
||||||
|
│ ├── App-specific failure (4 steps) │
|
||||||
|
│ └── Can't add printer (4 steps) │
|
||||||
|
│ │
|
||||||
|
│ [Save as Draft] [Publish] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI cost:** $0.00
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Total Cost Per Flow
|
||||||
|
|
||||||
|
| Stage | AI Calls | Estimated Cost |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. Foundation | 0 | $0.000 |
|
||||||
|
| 2. Structure Scaffolding | 1 | $0.003-0.005 |
|
||||||
|
| 3. Branch/Step Detail | 1 per branch (avg 5) | $0.015-0.025 |
|
||||||
|
| 4. Review & Refine | 0 | $0.000 |
|
||||||
|
| **Total** | **~6 calls** | **$0.018-0.030** |
|
||||||
|
|
||||||
|
**With prompt caching active** (system prompt cached across all users):
|
||||||
|
- First user of the day: ~$0.025 (full system prompt cost)
|
||||||
|
- Subsequent users: ~$0.012-0.018 (90% savings on system prompt input)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cost Projections By Plan
|
||||||
|
|
||||||
|
### Per-User Monthly Cost to ResolutionFlow
|
||||||
|
|
||||||
|
| Plan | AI Flows/Month | Est. Cost/Month | Subscription Price | Margin |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Free | 2 | $0.04-0.06 | $0 | -$0.06 (acquisition cost) |
|
||||||
|
| Pro | 30 (1/day) | $0.54-0.90 | $29 | 97%+ |
|
||||||
|
| Pro (heavy) | 300 (10/day) | $5.40-9.00 | $29 | 69-81% |
|
||||||
|
| Team (per user) | 300 (10/day) | $5.40-9.00 | ~$20/seat | 55-73% |
|
||||||
|
|
||||||
|
### At Scale (Monthly Platform Cost)
|
||||||
|
|
||||||
|
| Scenario | Users | Flows/Month | Monthly AI Cost |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Beta (15 users) | 15 | ~150 | $2.70-4.50 |
|
||||||
|
| Early growth | 100 | ~1,500 | $27-45 |
|
||||||
|
| Scaling | 500 | ~7,500 | $135-225 |
|
||||||
|
| Target ($10K MRR) | ~400 | ~6,000 | $108-180 |
|
||||||
|
|
||||||
|
**Bottom line:** Even at 10/day per user, AI costs stay under 30% of subscription revenue. At typical usage (1-3/day), AI costs are negligible — under 5% of revenue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan Limits Integration
|
||||||
|
|
||||||
|
### New Fields for plan_limits Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE plan_limits
|
||||||
|
ADD COLUMN ai_flows_per_month INTEGER DEFAULT NULL,
|
||||||
|
ADD COLUMN ai_calls_per_flow INTEGER DEFAULT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Plan | ai_flows_per_month | ai_calls_per_flow |
|
||||||
|
|---|---|---|
|
||||||
|
| Free | 2 | 6 |
|
||||||
|
| Pro | 50 | 10 |
|
||||||
|
| Team | 200 (per account) | 10 |
|
||||||
|
|
||||||
|
`ai_calls_per_flow` caps how many branches can get AI suggestions per flow. This prevents a user from creating a 50-branch monstrosity that costs $0.25 per generation.
|
||||||
|
|
||||||
|
### Tracking Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE ai_usage_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
account_id UUID NOT NULL REFERENCES accounts(id),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
usage_type VARCHAR(50) NOT NULL, -- 'flow_scaffold', 'branch_detail', 'branch_suggest'
|
||||||
|
model VARCHAR(100) NOT NULL, -- 'claude-haiku-4-5'
|
||||||
|
input_tokens INTEGER NOT NULL,
|
||||||
|
output_tokens INTEGER NOT NULL,
|
||||||
|
estimated_cost_usd NUMERIC(10, 6) NOT NULL,
|
||||||
|
flow_id UUID REFERENCES trees(id),
|
||||||
|
metadata JSONB DEFAULT '{}', -- stage, branch name, etc.
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ai_usage_account_month
|
||||||
|
ON ai_usage_log (account_id, created_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives you:
|
||||||
|
- Per-account usage tracking for limit enforcement
|
||||||
|
- Cost visibility in admin dashboard
|
||||||
|
- Data to optimize prompts over time (which calls use the most tokens?)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Implementation
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
services/
|
||||||
|
ai_flow_generator.py # Core AI service — prompt construction, API calls, parsing
|
||||||
|
api/
|
||||||
|
endpoints/
|
||||||
|
ai_generation.py # API endpoints for wizard stages
|
||||||
|
core/
|
||||||
|
ai_config.py # Model selection, pricing constants, feature flag checks
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/ai/flow/scaffold
|
||||||
|
Body: { flow_type, category_id, name, description, environment_tags }
|
||||||
|
Returns: { suggestions: ["symptom1", "symptom2", ...] }
|
||||||
|
Auth: Required, plan limit checked
|
||||||
|
AI: 1 Haiku call
|
||||||
|
|
||||||
|
POST /api/v1/ai/flow/branch-detail
|
||||||
|
Body: { flow_type, category_id, name, description, branch_name, existing_branches }
|
||||||
|
Returns: { steps: [ { type, title, action/question, help_text, sub_branches?, record_fields? } ] }
|
||||||
|
Auth: Required, plan limit checked, per-flow call limit checked
|
||||||
|
AI: 1 Haiku call
|
||||||
|
|
||||||
|
POST /api/v1/ai/flow/assemble
|
||||||
|
Body: { flow_type, category_id, name, description, branches: [ { name, steps: [...] } ] }
|
||||||
|
Returns: { tree_structure: { ... } } // Valid tree structure ready for create_tree
|
||||||
|
Auth: Required
|
||||||
|
AI: 0 calls — pure assembly logic, runs through normalize_node + validation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prompt Architecture
|
||||||
|
|
||||||
|
The system prompt is the same across all users and all calls within a stage. This is critical for prompt caching.
|
||||||
|
|
||||||
|
**System Prompt (Stage 2 — Troubleshooting):**
|
||||||
|
```
|
||||||
|
You are a senior MSP engineer helping build troubleshooting decision trees.
|
||||||
|
Given a problem description and category, suggest 4-7 initial symptom branches
|
||||||
|
that a support engineer would encounter.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Each suggestion should be a distinct, common symptom (not overlapping)
|
||||||
|
- Order from most common to least common
|
||||||
|
- Use plain language that a Tier 1 engineer would understand
|
||||||
|
- Focus on observable symptoms, not root causes
|
||||||
|
- Respond in JSON format only: { "suggestions": ["symptom1", "symptom2"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
**System Prompt (Stage 2 — Procedure):**
|
||||||
|
```
|
||||||
|
You are a senior MSP engineer helping build procedural checklists.
|
||||||
|
Given a project description and category, suggest 4-8 major phases
|
||||||
|
that the project should follow.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Phases should be in logical execution order
|
||||||
|
- Each phase should be a distinct stage of work
|
||||||
|
- Use standard MSP/IT terminology
|
||||||
|
- Respond in JSON format only: { "suggestions": ["phase1", "phase2"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
**System Prompt (Stage 3 — Branch Detail):**
|
||||||
|
```
|
||||||
|
You are a senior MSP engineer helping build troubleshooting steps.
|
||||||
|
Given a symptom/branch name and context, suggest 3-6 diagnostic steps.
|
||||||
|
|
||||||
|
For each step, provide:
|
||||||
|
- type: "action" (do something), "decision" (yes/no check), or "solution" (resolution)
|
||||||
|
- title: short step name
|
||||||
|
- content: detailed instructions including commands where relevant
|
||||||
|
- sub_branches: (for decisions only) what the yes/no paths look like
|
||||||
|
- record_fields: (for procedures only) data the engineer should document
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Include specific commands (PowerShell preferred for Windows environments)
|
||||||
|
- Action steps should be concrete and actionable
|
||||||
|
- Decision steps should have clear yes/no outcomes
|
||||||
|
- End branches with solution nodes where possible
|
||||||
|
- Respond in JSON format only
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caching strategy:** System prompts are sent as cacheable prefixes. With a 5-minute TTL, any user hitting the wizard within 5 minutes of another user gets 90% savings on input tokens. During active hours, this cache will almost always be warm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Implementation
|
||||||
|
|
||||||
|
### New Components
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/src/
|
||||||
|
pages/
|
||||||
|
FlowWizardPage.tsx # Main wizard container with stage navigation
|
||||||
|
components/
|
||||||
|
wizard/
|
||||||
|
WizardStageFoundation.tsx # Stage 1: type, category, name, description
|
||||||
|
WizardStageScaffold.tsx # Stage 2: AI suggestions with checkboxes
|
||||||
|
WizardStageBranchDetail.tsx # Stage 3: per-branch step suggestions
|
||||||
|
WizardStageReview.tsx # Stage 4: assembled flow preview
|
||||||
|
WizardProgress.tsx # Stage indicator bar
|
||||||
|
SuggestionCheckbox.tsx # Reusable AI suggestion with check/uncheck
|
||||||
|
CustomItemInput.tsx # "Add your own" input field
|
||||||
|
api/
|
||||||
|
aiGeneration.ts # API client for AI endpoints
|
||||||
|
hooks/
|
||||||
|
useAiWizard.ts # State management for wizard flow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wizard State Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WizardState {
|
||||||
|
// Stage 1
|
||||||
|
flowType: 'troubleshooting' | 'procedure';
|
||||||
|
categoryId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
environmentTags: string[];
|
||||||
|
|
||||||
|
// Stage 2
|
||||||
|
scaffoldSuggestions: string[]; // AI-generated
|
||||||
|
selectedBranches: string[]; // User-confirmed
|
||||||
|
customBranches: string[]; // User-added
|
||||||
|
|
||||||
|
// Stage 3
|
||||||
|
branchDetails: Record<string, BranchStep[]>; // Per-branch steps
|
||||||
|
currentBranchIndex: number;
|
||||||
|
|
||||||
|
// Stage 4
|
||||||
|
assembledStructure: TreeStructure | null;
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
currentStage: 1 | 2 | 3 | 4;
|
||||||
|
aiCallsUsed: number;
|
||||||
|
isGenerating: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UX Considerations
|
||||||
|
|
||||||
|
1. **Loading states:** AI calls take 2-5 seconds. Show a subtle loading animation with context ("Analyzing common symptoms..."), not a spinner.
|
||||||
|
|
||||||
|
2. **Graceful degradation:** If an AI call fails, show an empty state with "Add your own" prominent. The wizard should never be blocked by AI failure.
|
||||||
|
|
||||||
|
3. **Edit-in-place:** Users should be able to click any AI suggestion to edit the text before accepting it.
|
||||||
|
|
||||||
|
4. **Skip AI:** Every stage should have a "Skip suggestions, I'll add my own" option. Power users may want to build manually but still benefit from the wizard structure.
|
||||||
|
|
||||||
|
5. **Mobile consideration:** The wizard should work on tablet at minimum. MSP engineers may use it on-site with an iPad.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment & Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```
|
||||||
|
# .env / Railway
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
AI_GENERATION_MODEL=claude-haiku-4-5-20251001
|
||||||
|
AI_GENERATION_ENABLED=true # Kill switch
|
||||||
|
AI_MAX_RETRIES=2
|
||||||
|
AI_REQUEST_TIMEOUT=30 # seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Flag Integration
|
||||||
|
|
||||||
|
Use existing feature_flags system:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ai_flow_generation": {
|
||||||
|
"enabled": true,
|
||||||
|
"plans": ["pro", "team"],
|
||||||
|
"beta_override": true // Allow specific free users during beta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollout Strategy
|
||||||
|
|
||||||
|
### Phase 3a: Internal Testing (Week 1)
|
||||||
|
|
||||||
|
- Backend endpoints + prompt engineering
|
||||||
|
- Test with admin account only
|
||||||
|
- Iterate on prompt quality — this is where most time goes
|
||||||
|
- Validate cost estimates against real API usage
|
||||||
|
|
||||||
|
### Phase 3b: Beta Testing (Week 2-3)
|
||||||
|
|
||||||
|
- Frontend wizard UI
|
||||||
|
- Enable for beta testers (feature flag)
|
||||||
|
- Collect feedback on suggestion quality
|
||||||
|
- Monitor ai_usage_log for actual costs
|
||||||
|
|
||||||
|
### Phase 3c: General Availability (Week 4)
|
||||||
|
|
||||||
|
- Enable for all Pro/Team plans
|
||||||
|
- Free tier gets limited access (2/month)
|
||||||
|
- Admin dashboard shows AI cost metrics
|
||||||
|
- Plan limit enforcement active
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Not in Initial Scope)
|
||||||
|
|
||||||
|
These are explicitly deferred to keep initial scope manageable:
|
||||||
|
|
||||||
|
1. **AI-suggested branch improvements** — "Based on 15 unresolved sessions on this branch, AI suggests adding these steps." Requires session outcome tracking (separate feature).
|
||||||
|
|
||||||
|
2. **AI step refinement** — User selects an existing step and says "make this more detailed" or "add the PowerShell commands." Single-step AI call, very cheap.
|
||||||
|
|
||||||
|
3. **Template flows from wizard patterns** — If many users create similar flows (e.g., "printer troubleshooting"), cache the assembled structure as a template. Future users get instant results with zero AI cost.
|
||||||
|
|
||||||
|
4. **Multi-language generation** — Same wizard, but AI generates steps in the user's language. Prompt change only, no architecture change.
|
||||||
|
|
||||||
|
5. **AI from session data** — After enough sessions are tracked, use anonymized session paths to improve AI suggestions. "80% of engineers check the spooler service first" → AI learns to suggest that first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Risks & Mitigations
|
||||||
|
|
||||||
|
| Risk | Impact | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| Anthropic API outage | Wizard Stage 2-3 blocked | Graceful fallback to manual entry; AI is enhancement, not requirement |
|
||||||
|
| Poor suggestion quality | Users lose trust in feature | Extensive prompt testing before launch; user can always edit/override |
|
||||||
|
| Cost overrun from heavy usage | Margin erosion | Per-account monthly limits in plan_limits; ai_usage_log for monitoring |
|
||||||
|
| Prompt injection via user input | Unexpected AI output | Sanitize user input; structured JSON output parsing; never execute AI output as code |
|
||||||
|
| Haiku model deprecated | Service interruption | Model name in env variable, not hardcoded; swap to successor model |
|
||||||
|
| Users treat AI as authoritative | Bad troubleshooting steps followed blindly | "Draft" status by default; review stage required; clear "AI-suggested" labeling |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
| Metric | Target | How to Measure |
|
||||||
|
|---|---|---|
|
||||||
|
| Wizard completion rate | >70% of starts finish Stage 4 | Track stage progression in analytics |
|
||||||
|
| AI suggestion acceptance rate | >50% of suggestions kept | Compare suggestions shown vs. selected |
|
||||||
|
| Time to create flow (wizard vs manual) | 60%+ faster with wizard | Compare creation timestamps |
|
||||||
|
| Post-wizard editing rate | <30% of nodes edited after assembly | Compare Stage 4 structure vs. published version |
|
||||||
|
| Cost per flow | <$0.03 average | ai_usage_log aggregation |
|
||||||
|
| User satisfaction | Positive feedback from beta | Direct feedback + usage retention |
|
||||||
@@ -4,27 +4,34 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
/* Monochrome Design System — Dark Only */
|
/* ResolutionFlow Dark Theme — Purple Gradient Accents */
|
||||||
--background: 0 0% 0%;
|
--background: 240 10% 3.9%;
|
||||||
--foreground: 0 0% 100%;
|
--foreground: 0 0% 100%;
|
||||||
--card: 0 0% 4%;
|
--card: 240 10% 9.4%;
|
||||||
--card-foreground: 0 0% 100%;
|
--card-foreground: 0 0% 100%;
|
||||||
--popover: 0 0% 4%;
|
--popover: 240 10% 9.4%;
|
||||||
--popover-foreground: 0 0% 100%;
|
--popover-foreground: 0 0% 100%;
|
||||||
--primary: 0 0% 100%;
|
--primary: 243 75% 59%;
|
||||||
--primary-foreground: 0 0% 0%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--secondary: 0 0% 10%;
|
--secondary: 240 5.9% 15%;
|
||||||
--secondary-foreground: 0 0% 100%;
|
--secondary-foreground: 0 0% 100%;
|
||||||
--muted: 0 0% 10%;
|
--muted: 240 5.9% 15%;
|
||||||
--muted-foreground: 0 0% 50%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
--accent: 0 0% 8%;
|
--accent: 240 5.9% 15%;
|
||||||
--accent-foreground: 0 0% 100%;
|
--accent-foreground: 0 0% 100%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 100%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
--border: 0 0% 12%;
|
--border: 240 5.9% 15%;
|
||||||
--input: 0 0% 12%;
|
--input: 240 5.9% 15%;
|
||||||
--ring: 0 0% 100%;
|
--ring: 243 75% 59%;
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
|
/* App Shell tokens */
|
||||||
|
--sidebar-bg: 240 10% 4.5%;
|
||||||
|
--sidebar-hover: 240 6% 12%;
|
||||||
|
--sidebar-active: 243 75% 59% / 0.08;
|
||||||
|
--border-subtle: 240 6% 12%;
|
||||||
|
--text-dimmed: 240 4% 24%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
@@ -113,7 +120,14 @@
|
|||||||
@apply active:scale-[0.98] transition-transform;
|
@apply active:scale-[0.98] transition-transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glass card effect */
|
/* Brand gradient text */
|
||||||
|
.text-gradient-brand {
|
||||||
|
@apply bg-gradient-brand bg-clip-text text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Legacy glass-card utilities (preserved for backward compatibility) ── */
|
||||||
|
/* New components should use bg-card border-border rounded-xl instead */
|
||||||
|
|
||||||
.glass-card {
|
.glass-card {
|
||||||
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%);
|
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
@@ -125,14 +139,12 @@
|
|||||||
border-color: rgba(255, 255, 255, 0.12);
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active/highlighted card glow */
|
|
||||||
.glass-card-glow {
|
.glass-card-glow {
|
||||||
background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%);
|
background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
box-shadow: 0 0 40px rgba(255, 255, 255, 0.1);
|
box-shadow: 0 0 40px rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stat card */
|
|
||||||
.glass-stat {
|
.glass-stat {
|
||||||
background: rgba(20, 20, 25, 0.5);
|
background: rgba(20, 20, 25, 0.5);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
@@ -234,7 +246,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rdp-custom .rdp-day_selected {
|
.rdp-custom .rdp-day_selected {
|
||||||
@apply bg-white text-black hover:bg-white/90 hover:text-black;
|
@apply bg-primary text-primary-foreground hover:bg-primary/90;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rdp-custom .rdp-day_today {
|
.rdp-custom .rdp-day_today {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
"./index.html",
|
"./index.html",
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
@@ -7,7 +8,25 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// shadcn/ui color system (monochrome)
|
// ResolutionFlow Brand Colors
|
||||||
|
brand: {
|
||||||
|
gradient: {
|
||||||
|
from: '#818cf8',
|
||||||
|
to: '#a78bfa',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
DEFAULT: '#09090b',
|
||||||
|
card: '#18181b',
|
||||||
|
surface: '#12121c',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#a1a1aa',
|
||||||
|
muted: '#52525b',
|
||||||
|
},
|
||||||
|
border: '#27272a',
|
||||||
|
},
|
||||||
|
// shadcn/ui color system
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
input: "hsl(var(--input))",
|
input: "hsl(var(--input))",
|
||||||
ring: "hsl(var(--ring))",
|
ring: "hsl(var(--ring))",
|
||||||
@@ -49,6 +68,12 @@ export default {
|
|||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||||
|
heading: ['Plus Jakarta Sans', 'system-ui', 'sans-serif'],
|
||||||
|
label: ['Outfit', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-brand': 'linear-gradient(90deg, #818cf8 0%, #a78bfa 100%)',
|
||||||
|
'gradient-brand-hover': 'linear-gradient(90deg, #6366f1 0%, #9333ea 100%)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user