Spaces:
Runtime error
Runtime error
Đào Minh Tú commited on
Commit ·
52d338e
1
Parent(s): ea70636
ui(error-boxes): wrap long text in ScrollArea + break-words; doc rule
Browse files- .github/copilot-instructions.md +4 -0
- components/ab-compare.tsx +6 -3
- components/chat.tsx +8 -5
- components/error-log-dialog.tsx +1 -1
- components/header.tsx +1 -1
- components/mermaid-renderer.tsx +6 -3
- components/voice-mode.tsx +1 -1
.github/copilot-instructions.md
CHANGED
|
@@ -82,6 +82,10 @@ lib/
|
|
| 82 |
- Toast: dự án **không** dùng `sonner`. Dùng pattern toast local trong `sidebar.tsx` hoặc `Tooltip` (xem `share-link` impl).
|
| 83 |
- Header có nhiều nút icon → đã thêm `flex-1 overflow-hidden` ở container center + truncate trên PersonaPicker. Khi thêm nút mới phải đảm bảo `shrink-0` để không phá layout.
|
| 84 |
- Persona label: max `88px` truncate.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
### 7. Testing & build
|
| 87 |
|
|
|
|
| 82 |
- Toast: dự án **không** dùng `sonner`. Dùng pattern toast local trong `sidebar.tsx` hoặc `Tooltip` (xem `share-link` impl).
|
| 83 |
- Header có nhiều nút icon → đã thêm `flex-1 overflow-hidden` ở container center + truncate trên PersonaPicker. Khi thêm nút mới phải đảm bảo `shrink-0` để không phá layout.
|
| 84 |
- Persona label: max `88px` truncate.
|
| 85 |
+
- **Long text & error boxes**: bất kỳ container nào hiển thị message lỗi, URL dài, JSON, hoặc nội dung từ user/API đều phải:
|
| 86 |
+
1. Dùng `<ScrollArea className="max-h-XX">` (`@/components/ui/scroll-area`) thay vì `overflow-auto` thuần khi cần giới hạn chiều cao + có scrollbar đẹp.
|
| 87 |
+
2. Áp `break-words [overflow-wrap:anywhere]` (hoặc `whitespace-pre-wrap break-words`) lên text bên trong để URL/token dài tự xuống dòng — **không bao giờ** để text tràn ngang khỏi `border` của box.
|
| 88 |
+
3. Tách phần header (label) ra ngoài ScrollArea để label luôn cố định, chỉ phần body cuộn.
|
| 89 |
|
| 90 |
### 7. Testing & build
|
| 91 |
|
components/ab-compare.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm"
|
|
| 5 |
import { Check, Loader2 } from "lucide-react"
|
| 6 |
import { Button } from "@/components/ui/button"
|
| 7 |
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
|
|
|
|
| 8 |
import { cn } from "@/lib/utils"
|
| 9 |
|
| 10 |
export type AbPaneState = {
|
|
@@ -43,9 +44,11 @@ function AbCard({ label, state, onPick }: { label: string; state: AbPaneState; o
|
|
| 43 |
</CardHeader>
|
| 44 |
<CardContent className="min-h-40 p-3">
|
| 45 |
{state.error ? (
|
| 46 |
-
<
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
) : state.content ? (
|
| 50 |
<div className="prose-chat text-sm">
|
| 51 |
<ReactMarkdown remarkPlugins={[remarkGfm]}>{state.content}</ReactMarkdown>
|
|
|
|
| 5 |
import { Check, Loader2 } from "lucide-react"
|
| 6 |
import { Button } from "@/components/ui/button"
|
| 7 |
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
|
| 8 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 9 |
import { cn } from "@/lib/utils"
|
| 10 |
|
| 11 |
export type AbPaneState = {
|
|
|
|
| 44 |
</CardHeader>
|
| 45 |
<CardContent className="min-h-40 p-3">
|
| 46 |
{state.error ? (
|
| 47 |
+
<ScrollArea className="max-h-40 rounded-md border border-destructive/30 bg-destructive/5">
|
| 48 |
+
<div className="px-3 py-2 text-xs text-destructive break-words [overflow-wrap:anywhere]">
|
| 49 |
+
{state.error}
|
| 50 |
+
</div>
|
| 51 |
+
</ScrollArea>
|
| 52 |
) : state.content ? (
|
| 53 |
<div className="prose-chat text-sm">
|
| 54 |
<ReactMarkdown remarkPlugins={[remarkGfm]}>{state.content}</ReactMarkdown>
|
components/chat.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { BulkActions } from "@/components/bulk-actions"
|
|
| 14 |
import { MemoryDialog } from "@/components/memory-dialog"
|
| 15 |
import { PromptLibraryDialog } from "@/components/prompt-library-dialog"
|
| 16 |
import { ErrorLogDialog } from "@/components/error-log-dialog"
|
|
|
|
| 17 |
import { AbCompare, type AbPaneState } from "@/components/ab-compare"
|
| 18 |
import { AbToggle, type AbModeState } from "@/components/ab-toggle"
|
| 19 |
import { PROVIDERS, type ProviderKey } from "@/lib/providers"
|
|
@@ -731,11 +732,13 @@ export function Chat() {
|
|
| 731 |
!
|
| 732 |
</div>
|
| 733 |
<div className="min-w-0 flex-1 space-y-2 pt-1">
|
| 734 |
-
<div className="rounded-lg border border-destructive/30 bg-destructive/5
|
| 735 |
-
<div className="font-medium">Có lỗi khi gọi AI:</div>
|
| 736 |
-
<
|
| 737 |
-
|
| 738 |
-
|
|
|
|
|
|
|
| 739 |
</div>
|
| 740 |
<button
|
| 741 |
onClick={() => reload()}
|
|
|
|
| 14 |
import { MemoryDialog } from "@/components/memory-dialog"
|
| 15 |
import { PromptLibraryDialog } from "@/components/prompt-library-dialog"
|
| 16 |
import { ErrorLogDialog } from "@/components/error-log-dialog"
|
| 17 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 18 |
import { AbCompare, type AbPaneState } from "@/components/ab-compare"
|
| 19 |
import { AbToggle, type AbModeState } from "@/components/ab-toggle"
|
| 20 |
import { PROVIDERS, type ProviderKey } from "@/lib/providers"
|
|
|
|
| 732 |
!
|
| 733 |
</div>
|
| 734 |
<div className="min-w-0 flex-1 space-y-2 pt-1">
|
| 735 |
+
<div className="rounded-lg border border-destructive/30 bg-destructive/5 text-sm text-destructive">
|
| 736 |
+
<div className="px-3 pt-2 font-medium">Có lỗi khi gọi AI:</div>
|
| 737 |
+
<ScrollArea className="max-h-40">
|
| 738 |
+
<div className="px-3 pb-2 pt-1 text-xs opacity-90 whitespace-pre-wrap break-words [overflow-wrap:anywhere]">
|
| 739 |
+
{error.message}
|
| 740 |
+
</div>
|
| 741 |
+
</ScrollArea>
|
| 742 |
</div>
|
| 743 |
<button
|
| 744 |
onClick={() => reload()}
|
components/error-log-dialog.tsx
CHANGED
|
@@ -71,7 +71,7 @@ export function ErrorLogDialog({ open, onOpenChange }: ErrorLogDialogProps) {
|
|
| 71 |
{entry.request.provider ?? "unknown"} / {entry.request.model ?? "unknown"}
|
| 72 |
</span>
|
| 73 |
</div>
|
| 74 |
-
<div className="whitespace-pre-wrap text-sm text-destructive">{entry.error}</div>
|
| 75 |
</summary>
|
| 76 |
<pre className="mt-3 max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs">
|
| 77 |
{JSON.stringify(entry.request, null, 2)}
|
|
|
|
| 71 |
{entry.request.provider ?? "unknown"} / {entry.request.model ?? "unknown"}
|
| 72 |
</span>
|
| 73 |
</div>
|
| 74 |
+
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm text-destructive">{entry.error}</div>
|
| 75 |
</summary>
|
| 76 |
<pre className="mt-3 max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs">
|
| 77 |
{JSON.stringify(entry.request, null, 2)}
|
components/header.tsx
CHANGED
|
@@ -242,7 +242,7 @@ export function Header({
|
|
| 242 |
<DropdownMenuItem onClick={() => setSnippetsOpen(true)}>
|
| 243 |
<Bookmark className="mr-2 h-4 w-4" /> Snippet / Macro
|
| 244 |
</DropdownMenuItem>
|
| 245 |
-
<DropdownMenuItem onClick={toggleVoiceMode}>
|
| 246 |
<Mic className="mr-2 h-4 w-4" /> Chế độ giọng nói
|
| 247 |
</DropdownMenuItem>
|
| 248 |
<DropdownMenuItem onClick={() => setThemeOpen(true)}>
|
|
|
|
| 242 |
<DropdownMenuItem onClick={() => setSnippetsOpen(true)}>
|
| 243 |
<Bookmark className="mr-2 h-4 w-4" /> Snippet / Macro
|
| 244 |
</DropdownMenuItem>
|
| 245 |
+
<DropdownMenuItem onClick={toggleVoiceMode} className="hidden md:flex">
|
| 246 |
<Mic className="mr-2 h-4 w-4" /> Chế độ giọng nói
|
| 247 |
</DropdownMenuItem>
|
| 248 |
<DropdownMenuItem onClick={() => setThemeOpen(true)}>
|
components/mermaid-renderer.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"use client"
|
| 2 |
|
| 3 |
import { useEffect, useId, useMemo, useState } from "react"
|
|
|
|
| 4 |
|
| 5 |
type MermaidRendererProps = {
|
| 6 |
code: string
|
|
@@ -71,9 +72,11 @@ export function MermaidRenderer({ code, title }: MermaidRendererProps) {
|
|
| 71 |
<div className="h-40 w-full animate-pulse rounded-lg bg-muted" />
|
| 72 |
</div>
|
| 73 |
) : error ? (
|
| 74 |
-
<div className="rounded-lg border border-destructive/30 bg-destructive/10
|
| 75 |
-
<div className="font-medium">Không thể vẽ sơ đồ Mermaid</div>
|
| 76 |
-
<
|
|
|
|
|
|
|
| 77 |
</div>
|
| 78 |
) : (
|
| 79 |
<div
|
|
|
|
| 1 |
"use client"
|
| 2 |
|
| 3 |
import { useEffect, useId, useMemo, useState } from "react"
|
| 4 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 5 |
|
| 6 |
type MermaidRendererProps = {
|
| 7 |
code: string
|
|
|
|
| 72 |
<div className="h-40 w-full animate-pulse rounded-lg bg-muted" />
|
| 73 |
</div>
|
| 74 |
) : error ? (
|
| 75 |
+
<div className="rounded-lg border border-destructive/30 bg-destructive/10 text-sm text-destructive">
|
| 76 |
+
<div className="px-3 pt-3 font-medium">Không thể vẽ sơ đồ Mermaid</div>
|
| 77 |
+
<ScrollArea className="max-h-40">
|
| 78 |
+
<pre className="px-3 pb-3 pt-2 whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-xs">{error}</pre>
|
| 79 |
+
</ScrollArea>
|
| 80 |
</div>
|
| 81 |
) : (
|
| 82 |
<div
|
components/voice-mode.tsx
CHANGED
|
@@ -228,7 +228,7 @@ export function VoiceMode() {
|
|
| 228 |
<div className="mt-3 rounded-2xl bg-muted/50 p-3 text-xs">
|
| 229 |
{transcript && <p><span className="font-medium">Đang nghe:</span> {transcript}</p>}
|
| 230 |
{!transcript && lastSent && <p><span className="font-medium">Đã gửi:</span> {lastSent}</p>}
|
| 231 |
-
{error && <p className="mt-1 text-destructive">{error}</p>}
|
| 232 |
</div>
|
| 233 |
)}
|
| 234 |
|
|
|
|
| 228 |
<div className="mt-3 rounded-2xl bg-muted/50 p-3 text-xs">
|
| 229 |
{transcript && <p><span className="font-medium">Đang nghe:</span> {transcript}</p>}
|
| 230 |
{!transcript && lastSent && <p><span className="font-medium">Đã gửi:</span> {lastSent}</p>}
|
| 231 |
+
{error && <p className="mt-1 break-words [overflow-wrap:anywhere] text-destructive">{error}</p>}
|
| 232 |
</div>
|
| 233 |
)}
|
| 234 |
|