Đà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 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
- <div className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
47
- {state.error}
48
- </div>
 
 
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 px-3 py-2 text-sm text-destructive">
735
- <div className="font-medium">Có lỗi khi gọi AI:</div>
736
- <div className="mt-1 whitespace-pre-wrap text-xs opacity-90">
737
- {error.message}
738
- </div>
 
 
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 p-3 text-sm text-destructive">
75
- <div className="font-medium">Không thể vẽ sơ đồ Mermaid</div>
76
- <pre className="mt-2 whitespace-pre-wrap text-xs">{error}</pre>
 
 
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