fix(streaming): add proper headers for real-time chunk delivery

- Add Transfer-Encoding chunked and X-Accel-Buffering headers to chat and summarize handlers
- Start response immediately with c.Status(http.StatusOK)
- Fix SummaryDialog: wider desktop, reading font, no inner border, darker text
- Add generating animation while waiting for first LLM chunk
This commit is contained in:
rishikanthc
2025-12-14 11:14:05 -08:00
committed by Rishikanth Chandrasekaran
parent d8b6f4c023
commit 8cb6c394c8
11 changed files with 191 additions and 351 deletions

View File

@@ -1,65 +0,0 @@
---
trigger: always_on
---
The Fluid UI Style Guide
1. Deference & Negative Space
Principle: The UI must recede; content must dominate.
Action: Maximize white space. Avoid heavy borders or boxes to separate content. Use translucency (blur) for background layers to maintain context without visual noise.
2. Hierarchy via Typography
Principle: Group information using font weight and size, not lines.
Action: Establish a strict scale (e.g., Large Bold for titles, Medium for headers, Regular for body). Use 100% black for primary text and grey/opacity (e.g., 60%) for secondary text to guide the eye.
3. The 44pt Touch Standard
Principle: Accuracy allows for speed.
Action: All interactive tap targets must be at least 44x44 points, regardless of the visual icon size. Place primary navigation in the bottom "thumb zone."
4. Physics-Based Motion
Principle: Objects have mass and friction; nothing moves linearly.
Action: Use Spring Animations (configure mass, stiffness, damping) instead of ease-in/out. Ensure all animations are interruptible (if a user grabs a moving object, it stops instantly).
5. Color as Function
Principle: Color indicates interactivity, not decoration.
Action: Select one "Tint Color" (Accent) for buttons, links, and active states. Keep the structural UI monochrome (whites, greys, blacks).
6. Direct Manipulation (Gestures)
Principle: Users should manipulate the object, not a proxy for the object.
Action: Prioritize swipes (to delete/go back) and pinches over tap-based buttons. Ensure the animation tracks 1:1 with the user's finger during the gesture.
7. Depth & The Z-Axis
Principle: Interfaces are stacked layers, not flat planes.
Action: Use shadows and dimming to indicate elevation. When a modal or sheet appears, the background layer should scale down slightly or darken to push it "back" in Z-space.
8. Instant Multisensory Feedback
Principle: Every interaction requires acknowledgment.
Action: Response latency must be <100ms. Pair visual state changes (highlights/press states) with subtle haptic feedback (tactile bumps) for confirmation.
9. Zero Dead Ends (Empty States)
Principle: An empty screen is a broken experience.
Action: Never leave a container blank. Design illustrative "Empty States" that explain what belongs there and provide a direct button to create that content.
10. Functional Consistency
Principle: Predictability reduces cognitive load.
Action: Reuse system paradigms. If it looks like a switch, it must toggle. If it looks like a search bar, it must filter. Do not create custom controls if a standard system control exists.

View File

@@ -1,25 +0,0 @@
---
trigger: always_on
---
The "10 Commandments" of Go Backend Development
Accept Interfaces, Return Structs. Keep functions flexible by accepting behaviors (interfaces) and robust by returning data (structs). Let the consumer define the interface.
Propagate Context Everywhere. Pass context.Context as the first argument to every I/O function. It is essential for handling cancellation, timeouts, and tracing in distributed systems.
Wrap, Don't Hide, Errors. Never ignore errors. Wrap them with fmt.Errorf("%w", err) to add context (traceability) while allowing the caller to unwrap and inspect the root cause.
Package by Feature. Avoid models and controllers folders. Organize code by business domain (e.g., billing, auth) to ensure high cohesion and avoid circular dependencies.
Limit Concurrency. Never spawn unlimited goroutines per request. Use Worker Pools or semaphores to cap concurrency and prevent memory exhaustion under load.
Pre-allocate Memory. Use make([]T, 0, cap) when the size is known. Avoiding repeated array resizing reduces CPU overhead and garbage collection pressure.
Avoid Global State. Ban global variables and init() side effects. Use explicit Dependency Injection to make your server testable, modular, and thread-safe.
Pool Hot Objects. Use sync.Pool for frequently allocated short-lived objects (buffers, contexts). This drastically reduces Garbage Collection pauses in high-throughput systems.
Log Structurally. Abandon text logs. Use log/slog to emit JSON logs with key-value pairs (order_id=123), making your production logs queryable and actionable.
Write Table-Driven Tests. Utilize Gos idiom of table-driven testing to cover multiple edge cases cleanly and efficiently without duplicating test logic.

View File

@@ -1,33 +0,0 @@
---
trigger: always_on
---
1. **Separate Server State from Client State.**
Never store API data in Redux or Context. Use **TanStack Query** (or SWR) for caching and fetching, reserving **Zustand** or Context strictly for UI state (e.g., themes, modals).
2. **Organize by Feature, Not File Type.**
Abandon `/components` and `/hooks` folders. **Colocate** related code (api, hooks, components) into feature-specific directories (e.g., `features/user-profile`) to ensure maintainability as the codebase grows.
3. **Derive State, Do Not Sync It.**
Never use `useEffect` to update a state variable based on another state variable. Calculate the derived value directly in the render body to eliminate "glitches" and extra render cycles.
4. **Preserve Referential Equality.**
Wrap objects and functions passed to memoized children in **`useMemo`** and **`useCallback`**. Stable references prevent expensive, unnecessary re-renders of child components.
5. **Eliminate Render-Fetch Waterfalls.**
Do not chain data fetching (Parent fetches -> Renders -> Child fetches). Use route-level loaders or parallel queries to fetch all required data immediately.
6. **Prefer Composition Over Prop Drilling.**
Avoid passing data through five layers of components. Use **Component Composition** (passing components as `children` or props) to make the hierarchy flat and efficient.
7. **Virtualize Long Lists.**
Never render large datasets directly to the DOM. Use **TanStack Virtual** or `react-window` to render only the visible items, ensuring the browser remains responsive.
8. **Lazy Load Routes.**
Implement **Code Splitting** using `React.lazy` and `Suspense` for route-level components. This ensures users only download the JavaScript required for the current page.
9. **Enforce Strict TypeScript.**
Treat `any` as a compile error. Define explicit interfaces for all props and API responses to guarantee safe refactoring and self-documenting code.
10. **Isolate Logic in Custom Hooks.**
Keep UI components pure (JSX only). Abstract complex state logic, side effects, and listeners into **Custom Hooks** to ensure logic is testable, reusable, and readable.

View File

@@ -1,100 +0,0 @@
---
trigger: always_on
---
**Scriberr Premium Design System**.
This is a comprehensive set of design guidelines for the **Scriberr Design System**. These rules are tailored for your designer to ensure the "Raised/Floating" aesthetic works perfectly with a **Pure White** background and your specific **Orange Gradient** branding.
---
### **Scriberr Design System Guidelines**
#### **1. Depth & Elevation (The "Floating" Strategy)**
Since the main background is **Pure White (`#FFFFFF`)**, we cannot rely on background contrast to separate cards. We must rely on **Physics-Based Shadowing** and **Micro-Borders**.
* **The "Resting" State (Default Cards)**
* **Concept:** Objects should not look like they are flying; they should look like thick cardstock resting on a desk.
* **Guideline:** Use a **Dual-Shadow approach**:
1. *Ambient Shadow:* A very tight, low-blur shadow to ground the object (e.g., `0px 2px 4px`).
2. *Key Light Shadow:* A larger, very soft shadow to suggest depth (e.g., `0px 10px 20px`).
* **The "Micro-Border":** Every floating white card **must** have a `1px` solid border in a very pale grey (`rgba(0,0,0,0.06)`). This prevents the "ghosting" effect where white cards blend invisibly into the white background on low-quality monitors.
* **The "Active" State (Hover/Lift)**
* **Guideline:** When a user hovers over a card (like a list item), do not change the background color. Instead, **lift** the card.
* **Action:** Increase the shadow spread and slightly nudge the element up (`translateY(-2px)`). This mimics tactile feedback.
#### **2. Color Usage (The 60-30-10 Rule)**
Your **Orange Gradient** (`#FFAB40``#FF3D00`) is high-energy. It must be the "jewel" of the interface, not the wallpaper.
* **60% Neutral (The Canvas):**
* **Light Mode:** Strictly Pure White (`#FFFFFF`).
* **Dark Mode:** Deep Carbon (`#0A0A0A`).
* **30% Structure (Text & Lines):**
* Use shades of **Neutral Grey** (e.g., `#171717` to `#737373`).
* *Constraint:* Never use pure black (`#000000`) for text. It creates a harsh "strobe" effect against white backgrounds.
* **10% Accent (The Logo Colors):**
* **Primary Action:** Use the **Orange Gradient** for the single most important action on a screen (e.g., "New Recording," "Save").
* **Secondary Action:** Use the **Solid Middle Orange** (`#FF6D20`) for links or active icons.
* **Tertiary/Backgrounds:** Use a "Washed Orange" (`rgba(255, 171, 64, 0.08)`) for active states in menus or selected items to tie them to the brand without overwhelming the eye.
#### **3. Typography & Hierarchy**
To maintain the "Sleek/Premium" feel, hierarchy is established through **Weight**, not just Size.
* **Headings:** Use a geometric sans-serif (like Inter, DM Sans, or Plus Jakarta).
* *Style:* Bold or ExtraBold.
* *Color:* Darkest Grey (`#171717`).
* **Body Text:**
* *Style:* Regular.
* *Color:* Medium Grey (`#525252`).
* *Constraint:* Keep line-height generous (approx 1.5 to 1.6) to maintain the "airiness" of the white background.
* **Metadata (Dates, file sizes):**
* *Style:* Medium Weight, Smaller Size.
* *Color:* Light Grey (`#A3A3A3`).
* *Transformation:* Use Uppercase + Wide Letter Spacing (Tracking) occasionally for labels (e.g., "AUDIO FILES") to add elegance.
#### **4. Iconography**
Icons must bridge the gap between your detailed logo and the minimal UI.
* **Style:** Use **Rounded** or **Soft-Edge** strokes (2px width). Avoid sharp, jagged icons.
* **Coloring Strategy:**
* *Inactive Icons:* Slate Grey (`#A3A3A3`).
* *Active Icons:* Solid Orange (`#FF6D20`).
* *Feature Icons:* If you need a large icon (e.g., empty state placeholder), apply the **Brand Gradient** to the icon stroke for a premium touch.
#### **5. Dark Mode Specifics (The "Achromatic" Rule)**
Since you requested **no blues**, the dark mode must rely on "Surface Lightness."
* **The "Rim Light" Technique:**
* In the absence of shadows, every card in Dark Mode must have a `1px` border of `rgba(255, 255, 255, 0.08)`. This mimics light catching the edge of the plastic/metal.
* **Elevation Mapping:**
* *Background:* `#0A0A0A` (Deepest)
* *Card Level 1:* `#141414` (Slightly Lighter)
* *Card Level 2 (Modals/Dropdowns):* `#1F1F1F` (Lighter still)
* **Text Contrast:**
* Do not use pure white (`#FFFFFF`) text on dark backgrounds; it causes "halation" (blurring). Use `#EDEDED` (93% White).
#### **6. Interactive Components**
* **Buttons:**
* *Primary:* Full Gradient background. Text is White.
* *Secondary:* White background, Grey border, Dark text. Hover border changes to Orange.
* *Ghost:* Transparent background, Orange text.
* **Inputs (Text Fields):**
* *Default:* Light Grey background (`#F9FAFB`) with no border. This feels softer than a harsh outlined box.
* *Focus:* White background + Orange Border + Orange "Glow" shadow.
#### **7. Status Colors (Harmonization)**
Standard "Traffic Light" colors often clash with custom branding. Use this adjusted palette to harmonize with your Orange logo:
* **Success:** **Teal/Emerald** (`#10B981`). (Standard Green fights with Orange; Teal complements it).
* **Error:** **Warm Red** (`#EF4444`). (Standard bright red is too aggressive; Warm red matches the "heat" of the orange).
* **Warning:** **Amber** (`#F59E0B`). (Naturally fits the palette).
#### **7. The CSS Theme Block**
The custom theme has been added to the index.css. Use these specifiers to style all UI components. All style setting values should be defined centrally in index.css
Colors should never be hardcoded.
A set of carefully designed components are there in @/components/ui folder. try to reuse them as much as you can for consistency. If what you need is not there then feel free to build a custom one.

View File

@@ -1,5 +0,0 @@
---
description:
---
Run build.sh in the project root and verify it builds successfully without errors

View File

@@ -1,7 +0,0 @@
---
description: commit changes
---
Update .gitignore if needed
Stage all required files
Commit all changes with a good description that answers what or why

View File

@@ -573,13 +573,17 @@ func (h *Handler) SendChatMessage(c *gin.Context) {
}
// Set up streaming response with context info headers
c.Header("Content-Type", "text/plain")
c.Header("Cache-Control", "no-cache")
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Connection", "keep-alive")
c.Header("Transfer-Encoding", "chunked")
c.Header("X-Accel-Buffering", "no") // Disable nginx buffering
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Expose-Headers", "X-Context-Used, X-Context-Limit, X-Messages-Trimmed")
c.Header("X-Context-Used", fmt.Sprintf("%d", currentTokenCount))
c.Header("X-Context-Limit", fmt.Sprintf("%d", contextWindow))
c.Header("X-Messages-Trimmed", fmt.Sprintf("%d", trimmedCount))
c.Status(http.StatusOK) // Start the response immediately
// Stream the response
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)

View File

@@ -55,10 +55,13 @@ func (h *Handler) Summarize(c *gin.Context) {
start := time.Now()
log.Printf("[summarize] start transcription_id=%s provider=%s model=%s content_len=%d", req.TranscriptionID, provider, req.Model, len(req.Content))
// Stream response
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
// Stream response with proper headers for real-time delivery
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Connection", "keep-alive")
c.Header("Transfer-Encoding", "chunked")
c.Header("X-Accel-Buffering", "no") // Disable nginx buffering
c.Status(http.StatusOK) // Start response immediately
// Allow longer generation time for large transcripts and smaller models
ctx, cancel := context.WithTimeout(c.Request.Context(), 60*time.Minute)

View File

@@ -1,4 +0,0 @@
We are building an audio transcription app called Scriberr using Go and React.
I want you to analyze and udnerstand the react frontend code in web/frontend and fix ALL lint errors in the code.
<IMPORTANT> Do not break any form function or feature.. UI and UX should remain exactly the same. All features should work.
DO NOT INTRODUCE REGRESSIONS OR BUGS</IMPORTANT>

View File

@@ -174,10 +174,12 @@ export function ChatSessionsSidebar({
<Plus className="h-5 w-5" />
</Button>
</DialogTrigger>
<DialogContent className="w-[calc(100%-2rem)] max-w-md mx-auto bg-[var(--bg-card)] dark:bg-[#0A0A0A] border border-[var(--border-subtle)] shadow-[var(--shadow-float)] p-0 rounded-2xl overflow-hidden">
<DialogContent className="w-[calc(100%-2rem)] max-w-md mx-auto bg-[var(--bg-card)] dark:bg-[#0A0A0A] border border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] shadow-[0_2px_4px_rgba(0,0,0,0.04),0_24px_48px_rgba(0,0,0,0.08)] dark:shadow-[0_2px_4px_rgba(0,0,0,0.3),0_24px_48px_rgba(0,0,0,0.3)] p-0 rounded-2xl overflow-hidden">
<DialogHeader className="p-5 pb-0">
<DialogTitle className="text-xl font-bold text-[var(--text-primary)] flex items-center gap-2">
<Sparkles className="h-5 w-5 text-[var(--brand-solid)]" />
<div className="h-9 w-9 rounded-full bg-gradient-to-br from-[#FFAB40] to-[#FF6D20] flex items-center justify-center shadow-md">
<Sparkles className="h-4 w-4 text-white" />
</div>
New Chat Session
</DialogTitle>
<p className="text-sm text-[var(--text-tertiary)] mt-1">
@@ -268,8 +270,8 @@ export function ChatSessionsSidebar({
className={`
group relative p-3 rounded-xl border cursor-pointer transition-all duration-200 pr-10 min-h-[64px]
${session.id === activeSessionId
? 'bg-card border-[#FF6D20] shadow-md ring-1 ring-[#FF6D20]/20 z-10'
: 'bg-card border-border/60 shadow-md hover:border-primary/50 hover:bg-card/80'
? 'bg-[var(--bg-card)] dark:bg-[#1F1F1F] border-[#FF6D20] shadow-[0_2px_4px_rgba(0,0,0,0.04),0_8px_16px_rgba(0,0,0,0.06)] dark:shadow-[0_2px_4px_rgba(0,0,0,0.3),0_8px_16px_rgba(0,0,0,0.2)] ring-1 ring-[#FF6D20]/20 z-10'
: 'bg-[var(--bg-card)] dark:bg-[#141414] border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] shadow-[0_2px_4px_rgba(0,0,0,0.04),0_8px_16px_rgba(0,0,0,0.04)] dark:shadow-[0_2px_4px_rgba(0,0,0,0.2),0_8px_16px_rgba(0,0,0,0.1)] hover:shadow-[0_4px_8px_rgba(0,0,0,0.06),0_12px_24px_rgba(0,0,0,0.06)] hover:-translate-y-0.5 hover:border-[var(--brand-solid)]/30'
}
`}
>

View File

@@ -31,7 +31,7 @@ import { useSummaryTemplates, useSummarizer, useExistingSummary } from "@/featur
import { useTranscript, useAudioDetail } from "@/features/transcription/hooks/useAudioDetail";
import { Sparkles, Download, Copy, RefreshCw, ChevronDown } from "lucide-react";
import { Sparkles, Download, Copy, RefreshCw, ChevronDown, FileText } from "lucide-react";
interface SummaryDialogProps {
audioId: string;
@@ -43,7 +43,7 @@ interface SummaryDialogProps {
export function SummaryDialog({ audioId, isOpen, onClose, llmReady }: SummaryDialogProps) {
const { toast } = useToast();
const { data: templates = [], isLoading: templatesLoading } = useSummaryTemplates();
const { data: existingSummary } = useExistingSummary(audioId);
const { data: existingSummary, isLoading: summaryLoading } = useExistingSummary(audioId);
const { data: transcript } = useTranscript(audioId, true);
const { data: audioFile } = useAudioDetail(audioId);
@@ -58,11 +58,20 @@ export function SummaryDialog({ audioId, isOpen, onClose, llmReady }: SummaryDia
const selectedTemplate = templates.find(t => t.id === selectedTemplateId);
// Auto-show existing summary if available and not streaming
// Wait for loading to complete to prevent blank display
useEffect(() => {
if (isOpen && existingSummary && !isStreaming && !streamContent) {
if (isOpen && !summaryLoading && existingSummary?.content && !isStreaming && !streamContent) {
setShowOutput(true);
}
}, [isOpen, existingSummary, isStreaming, streamContent]);
}, [isOpen, existingSummary, summaryLoading, isStreaming, streamContent]);
// Reset state when dialog closes
useEffect(() => {
if (!isOpen) {
setShowOutput(false);
setSelectedTemplateId("");
}
}, [isOpen]);
const handleStartSummary = () => {
if (!selectedTemplate || !transcript) return;
@@ -98,134 +107,193 @@ export function SummaryDialog({ audioId, isOpen, onClose, llmReady }: SummaryDia
URL.revokeObjectURL(url);
};
// Handle close - prevent closing during streaming
const handleOpenChange = (open: boolean) => {
if (!open && isStreaming) {
// Don't allow closing during streaming
return;
}
onClose(open);
};
if (showOutput) {
// Output View
// Output View - Redesigned with Scriberr Design System
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-3xl bg-[var(--bg-card)] border-[var(--border-subtle)] shadow-[var(--shadow-float)] max-h-[85vh] overflow-y-auto">
<DialogHeader className="border-b border-[var(--border-subtle)] pb-4">
<DialogTitle className="text-[var(--text-primary)] flex items-center gap-2">
<Sparkles className="h-5 w-5 text-[var(--brand-solid)]" />
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="w-[calc(100%-2rem)] max-w-4xl mx-auto bg-[var(--bg-card)] dark:bg-[#0A0A0A] border border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] shadow-[0_2px_4px_rgba(0,0,0,0.04),0_24px_48px_rgba(0,0,0,0.08)] dark:shadow-[0_2px_4px_rgba(0,0,0,0.3),0_24px_48px_rgba(0,0,0,0.3)] p-0 rounded-2xl max-h-[85vh] overflow-hidden">
<DialogHeader className="p-5 pb-4 border-b border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)]">
<DialogTitle className="text-xl font-bold text-[var(--text-primary)] flex items-center gap-2">
<div className="h-9 w-9 rounded-full bg-gradient-to-br from-[#FFAB40] to-[#FF6D20] flex items-center justify-center shadow-md">
<Sparkles className="h-4 w-4 text-white" />
</div>
Summary
</DialogTitle>
<DialogDescription className="flex items-center gap-2 text-[var(--text-secondary)]">
<DialogDescription className="flex items-center gap-2 text-[var(--text-secondary)] mt-1">
{isStreaming ? (
<>
<span>Generating summary...</span>
<span className="inline-block h-3.5 w-3.5 border-2 border-[var(--brand-solid)] border-t-transparent rounded-full animate-spin" aria-label="Loading" />
<span>Generating summary</span>
<span className="flex gap-1">
<span className="w-1.5 h-1.5 bg-[var(--brand-solid)] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-1.5 h-1.5 bg-[var(--brand-solid)] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-1.5 h-1.5 bg-[var(--brand-solid)] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</span>
</>
) : (
<span>Summary {error ? 'failed' : 'ready'}</span>
<span>{error ? 'Generation failed' : 'Summary ready'}</span>
)}
</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-end gap-2 mb-2 pt-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setShowOutput(false);
setSelectedTemplateId('');
}}
disabled={isStreaming}
>
<RefreshCw className="h-3.5 w-3.5" />
Regenerate
</Button>
<Button
variant="outline"
size="sm"
onClick={handleCopy}
disabled={!streamContent && !existingSummary?.content}
>
<Copy className="h-3.5 w-3.5" />
Copy
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDownload}
disabled={!streamContent && !existingSummary?.content}
>
<Download className="h-3.5 w-3.5" />
Download
</Button>
</div>
<div className="prose prose-stone dark:prose-invert max-w-none min-h-[200px] p-4 bg-[var(--bg-main)] rounded-[var(--radius-card)] border border-[var(--border-subtle)]">
{error ? (
<p className="text-sm text-[var(--error)]">{error}</p>
) : (
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeRaw as any, rehypeKatex as any, rehypeHighlight as any]}
components={{
// Override typography colors if prose doesn't handle vars well
p: ({ node, ...props }) => <p className="text-[var(--text-secondary)] leading-7" {...props} />,
h1: ({ node, ...props }) => <h1 className="text-[var(--text-primary)] font-bold text-2xl mt-6 mb-4" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-[var(--text-primary)] font-bold text-xl mt-6 mb-3" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-[var(--text-primary)] font-bold text-lg mt-5 mb-2" {...props} />,
li: ({ node, ...props }) => <li className="text-[var(--text-secondary)]" {...props} />,
strong: ({ node, ...props }) => <strong className="text-[var(--text-primary)] font-bold" {...props} />,
<div className="p-5 pt-4">
{/* Action buttons */}
<div className="flex flex-wrap items-center justify-end gap-2 mb-4">
<Button
variant="outline"
size="sm"
onClick={() => {
setShowOutput(false);
setSelectedTemplateId('');
}}
disabled={isStreaming}
className="h-9 rounded-full border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] hover:bg-[var(--bg-main)] transition-all"
>
{streamContent || existingSummary?.content || ""}
</ReactMarkdown>
)}
{!error && !streamContent && !existingSummary?.content && isStreaming && (
<p className="text-sm text-[var(--text-tertiary)] italic">Generating summary...</p>
)}
<RefreshCw className="h-3.5 w-3.5" />
Regenerate
</Button>
<Button
variant="outline"
size="sm"
onClick={handleCopy}
disabled={!streamContent && !existingSummary?.content}
className="h-9 rounded-full border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] hover:bg-[var(--bg-main)] transition-all"
>
<Copy className="h-3.5 w-3.5" />
Copy
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDownload}
disabled={!streamContent && !existingSummary?.content}
className="h-9 rounded-full border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] hover:bg-[var(--bg-main)] transition-all"
>
<Download className="h-3.5 w-3.5" />
Download
</Button>
</div>
{/* Content area - no inner card, full width, reading font */}
<div className="min-h-[200px] max-h-[55vh] overflow-y-auto font-reading">
{error ? (
<p className="text-sm text-[var(--error)]">{error}</p>
) : isStreaming && !streamContent ? (
/* Generating animation while waiting for first chunk */
<div className="flex flex-col items-center justify-center py-12 text-[var(--text-tertiary)]">
<div className="relative h-12 w-12 mb-4">
<div className="absolute inset-0 rounded-full border-2 border-[var(--brand-solid)]/20"></div>
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-[var(--brand-solid)] animate-spin"></div>
<Sparkles className="absolute inset-0 m-auto h-5 w-5 text-[var(--brand-solid)] animate-pulse" />
</div>
<p className="text-sm font-medium">Generating summary...</p>
<p className="text-xs mt-1">This may take a moment</p>
</div>
) : (
<div className="prose prose-stone dark:prose-invert max-w-none text-[#171717] dark:text-[#EDEDED] leading-relaxed">
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeRaw as any, rehypeKatex as any, rehypeHighlight as any]}
components={{
p: ({ node, ...props }) => <p className="text-[#525252] dark:text-[#A3A3A3] leading-7 mb-4" {...props} />,
h1: ({ node, ...props }) => <h1 className="text-[#171717] dark:text-[#EDEDED] font-bold text-2xl mt-6 mb-4" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-[#171717] dark:text-[#EDEDED] font-bold text-xl mt-6 mb-3" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-[#171717] dark:text-[#EDEDED] font-bold text-lg mt-5 mb-2" {...props} />,
li: ({ node, ...props }) => <li className="text-[#525252] dark:text-[#A3A3A3] mb-1" {...props} />,
strong: ({ node, ...props }) => <strong className="text-[#171717] dark:text-[#EDEDED] font-bold" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc pl-5 mb-4" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal pl-5 mb-4" {...props} />,
}}
>
{streamContent || existingSummary?.content || ""}
</ReactMarkdown>
{isStreaming && (
<span className="inline-block w-2 h-5 bg-[var(--brand-solid)] ml-0.5 animate-pulse align-middle" />
)}
</div>
)}
{!error && !streamContent && !existingSummary?.content && !isStreaming && (
<p className="text-sm text-[var(--text-tertiary)] italic text-center py-8">No content to display.</p>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}
// Template Selector View
// Template Selector View - Redesigned with Scriberr Design System
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg bg-[var(--bg-card)] border-[var(--border-subtle)] shadow-[var(--shadow-float)]">
<DialogHeader>
<DialogTitle className="text-[var(--text-primary)]">Summarize Transcript</DialogTitle>
<DialogDescription className="text-[var(--text-secondary)]">Choose a summarization template to generate insights.</DialogDescription>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent
className="w-[calc(100%-2rem)] max-w-lg mx-auto bg-[var(--bg-card)] dark:bg-[#0A0A0A] border border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] shadow-[0_2px_4px_rgba(0,0,0,0.04),0_24px_48px_rgba(0,0,0,0.08)] dark:shadow-[0_2px_4px_rgba(0,0,0,0.3),0_24px_48px_rgba(0,0,0,0.3)] p-0 rounded-2xl overflow-hidden"
onPointerDownOutside={(e) => {
// Prevent closing when clicking inside popover
if (tplPopoverOpen) {
e.preventDefault();
}
}}
>
<DialogHeader className="p-5 pb-0">
<DialogTitle className="text-xl font-bold text-[var(--text-primary)] flex items-center gap-2">
<div className="h-9 w-9 rounded-full bg-gradient-to-br from-[#FFAB40] to-[#FF6D20] flex items-center justify-center shadow-md">
<FileText className="h-4 w-4 text-white" />
</div>
Summarize Transcript
</DialogTitle>
<DialogDescription className="text-[var(--text-tertiary)] mt-1">
Choose a summarization template to generate insights
</DialogDescription>
</DialogHeader>
{llmReady === false && (
<div className="p-3 bg-[var(--warning-translucent)] text-[var(--warning-solid)] border border-[var(--warning-solid)]/20 rounded-[var(--radius-card)] text-sm mb-2">
LLM is not configured or active. Please check settings.
</div>
)}
<div className="p-5 space-y-5">
{llmReady === false && (
<div className="p-4 bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 rounded-xl text-sm flex items-center gap-2">
<span className="h-2 w-2 bg-amber-500 rounded-full animate-pulse" />
LLM is not configured or active. Please check settings.
</div>
)}
<div className="py-4 space-y-4">
<div className="space-y-1.5 local-form-group">
<div className="space-y-2">
<Label className="text-sm font-medium text-[var(--text-secondary)]">Template</Label>
<Popover open={tplPopoverOpen} onOpenChange={setTplPopoverOpen}>
<Popover open={tplPopoverOpen} onOpenChange={setTplPopoverOpen} modal={true}>
<PopoverTrigger asChild>
<button
className="w-full inline-flex justify-between items-center rounded-[var(--radius-card)] border border-[var(--border-subtle)] bg-[var(--bg-main)] px-3 py-2.5 text-sm text-[var(--text-primary)] hover:border-[var(--brand-solid)]/50 focus:ring-2 focus:ring-[var(--brand-solid)]/20 transition-all outline-none disabled:opacity-50"
className="w-full h-11 inline-flex justify-between items-center rounded-xl border border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] bg-[var(--bg-main)] dark:bg-[#141414] px-4 text-sm text-[var(--text-primary)] hover:border-[var(--brand-solid)]/50 focus:ring-2 focus:ring-[var(--brand-solid)]/20 transition-all outline-none disabled:opacity-50 shadow-[0_2px_4px_rgba(0,0,0,0.04)]"
aria-label="Choose template"
disabled={!llmReady}
type="button"
>
<span className="truncate text-left">{selectedTemplate ? selectedTemplate.name : (templatesLoading ? 'Loading...' : 'Select a template')}</span>
<span className="flex items-center text-xs text-[var(--text-tertiary)] ml-2 truncate">
<span className="flex items-center text-xs text-[var(--text-tertiary)] ml-2 shrink-0">
{selectedTemplate?.model ? `(${selectedTemplate.model})` : ''}
<ChevronDown className="ml-2 h-4 w-4 opacity-50" />
</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1 bg-[var(--bg-card)] border border-[var(--border-subtle)] shadow-xl rounded-[var(--radius-card)]">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-1 bg-[var(--bg-card)] dark:bg-[#1F1F1F] border border-[rgba(0,0,0,0.06)] dark:border-[rgba(255,255,255,0.08)] shadow-xl rounded-xl"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<Command className="bg-transparent">
<CommandInput placeholder="Search templates..." className="border-none focus:ring-0" />
<CommandInput placeholder="Search templates..." className="border-none focus:ring-0 h-10" />
<CommandList className="max-h-64 overflow-auto p-1">
<CommandEmpty className="py-2 text-center text-xs text-[var(--text-tertiary)]">{templatesLoading ? 'Loading...' : 'No templates found'}</CommandEmpty>
<CommandEmpty className="py-3 text-center text-xs text-[var(--text-tertiary)]">{templatesLoading ? 'Loading...' : 'No templates found'}</CommandEmpty>
<CommandGroup heading="Templates" className="text-[var(--text-tertiary)]">
{templates.map(t => (
<CommandItem
key={t.id}
value={t.name}
onSelect={() => { setSelectedTemplateId(t.id); setTplPopoverOpen(false); }}
className="rounded-sm aria-selected:bg-[var(--brand-solid)] aria-selected:text-white cursor-pointer"
className="rounded-lg py-2.5 px-3 aria-selected:bg-[var(--brand-solid)] aria-selected:text-white cursor-pointer transition-colors"
>
<div className="flex flex-col w-full">
<span className="text-sm font-medium">{t.name}</span>
@@ -246,22 +314,24 @@ export function SummaryDialog({ audioId, isOpen, onClose, llmReady }: SummaryDia
<p className="text-xs text-[var(--error)] pl-1">Selected template has no model configured.</p>
)}
</div>
</div>
<div className="mt-4 flex items-center justify-end gap-3 pt-2">
<Button
variant="ghost"
onClick={() => onClose(false)}
>
Cancel
</Button>
<Button
variant="brand"
disabled={!selectedTemplateId || !selectedTemplate?.model || !llmReady}
onClick={handleStartSummary}
>
Generate Summary
</Button>
</div>
<div className="p-5 pt-0 flex flex-col-reverse sm:flex-row gap-3 sm:justify-end">
<Button
variant="ghost"
onClick={() => onClose(false)}
className="h-11 px-6 rounded-full text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-main)] w-full sm:w-auto"
>
Cancel
</Button>
<Button
disabled={!selectedTemplateId || !selectedTemplate?.model || !llmReady}
onClick={handleStartSummary}
className="h-11 px-6 bg-gradient-to-br from-[#FFAB40] to-[#FF3D00] text-white hover:scale-[1.02] active:scale-[0.98] transition-transform shadow-md disabled:opacity-50 disabled:cursor-not-allowed rounded-full w-full sm:w-auto"
>
<Sparkles className="h-4 w-4 mr-2" />
Generate Summary
</Button>
</div>
</DialogContent>
</Dialog>