mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-06-28 06:46:25 +00:00
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:
committed by
Rishikanth Chandrasekaran
parent
d8b6f4c023
commit
8cb6c394c8
@@ -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.
|
||||
@@ -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 Go’s idiom of table-driven testing to cover multiple edge cases cleanly and efficiently without duplicating test logic.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description:
|
||||
---
|
||||
|
||||
Run build.sh in the project root and verify it builds successfully without errors
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user