Đào Minh Tú Copilot commited on
Commit
adb9e89
·
1 Parent(s): ca0e286

feat(composer): default AI toggles ON + toast feedback on click

Browse files

- Self-critique and auto-compact-context now default to ON for new
visitors (first-time = no localStorage entry write 'true' so it
sticks; existing users keep their previous choice).
- Clicking either icon now fires a sonner toast describing what just
changed and what the feature does, so users actually know what those
little icons under the input do.

Adds 'sonner' dep + <Toaster /> in app/layout.tsx (bottom-center, rich
colors, theme-aware, dismissible).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Files changed (4) hide show
  1. app/layout.tsx +2 -0
  2. components/composer.tsx +51 -6
  3. package-lock.json +11 -0
  4. package.json +1 -0
app/layout.tsx CHANGED
@@ -6,6 +6,7 @@ import { ThemeProvider } from "@/components/theme-provider"
6
  import { AuthProvider } from "@/components/auth-provider"
7
  import { PWARegister } from "@/components/pwa-register"
8
  import { VoiceMode } from "@/components/voice-mode"
 
9
  import "./globals.css"
10
 
11
  export const metadata: Metadata = {
@@ -38,6 +39,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
38
  <TooltipProvider delayDuration={300}>
39
  {children}
40
  <VoiceMode />
 
41
  </TooltipProvider>
42
  </ThemeProvider>
43
  </AuthProvider>
 
6
  import { AuthProvider } from "@/components/auth-provider"
7
  import { PWARegister } from "@/components/pwa-register"
8
  import { VoiceMode } from "@/components/voice-mode"
9
+ import { Toaster } from "sonner"
10
  import "./globals.css"
11
 
12
  export const metadata: Metadata = {
 
39
  <TooltipProvider delayDuration={300}>
40
  {children}
41
  <VoiceMode />
42
+ <Toaster position="bottom-center" theme="system" richColors closeButton />
43
  </TooltipProvider>
44
  </ThemeProvider>
45
  </AuthProvider>
components/composer.tsx CHANGED
@@ -18,6 +18,7 @@ import { searchActiveCollection } from "@/lib/collections"
18
  import { countTokens } from "@/lib/tokens"
19
  import { useSpeechRecognition } from "@/hooks/use-voice"
20
  import { cn } from "@/lib/utils"
 
21
 
22
  type QuoteEventDetail = {
23
  text?: string
@@ -571,8 +572,22 @@ export function Composer({ value, onChange, onSubmit, onStop, isStreaming, disab
571
  </div>
572
  </div>
573
  <div className="mt-1.5 flex items-center justify-end gap-1 px-2 text-[10px] text-muted-foreground">
574
- <AiFeatureToggle storageKey="tans:self-critique" label="Tự đánh giá câu trả lời" icon={<Brain className="h-3.5 w-3.5" />} />
575
- <AiFeatureToggle storageKey="tans:auto-compact" label="Tự nén context khi đầy" icon={<FolderArchive className="h-3.5 w-3.5" />} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  <span className="mx-1 h-3 w-px bg-border/60" aria-hidden />
577
  <Tooltip>
578
  <TooltipTrigger asChild>
@@ -616,16 +631,46 @@ export function Composer({ value, onChange, onSubmit, onStop, isStreaming, disab
616
  )
617
  }
618
 
619
- function AiFeatureToggle({ storageKey, label, icon }: { storageKey: string; label: string; icon: React.ReactNode }) {
620
- const [enabled, setEnabled] = useState(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  useEffect(() => {
622
  if (typeof window === "undefined") return
623
- setEnabled(window.localStorage.getItem(storageKey) === "true")
624
- }, [storageKey])
 
 
 
 
 
 
 
625
  function toggle() {
626
  const next = !enabled
627
  setEnabled(next)
628
  if (typeof window !== "undefined") window.localStorage.setItem(storageKey, String(next))
 
 
 
 
 
 
 
 
 
629
  }
630
  return (
631
  <Tooltip>
 
18
  import { countTokens } from "@/lib/tokens"
19
  import { useSpeechRecognition } from "@/hooks/use-voice"
20
  import { cn } from "@/lib/utils"
21
+ import { toast } from "sonner"
22
 
23
  type QuoteEventDetail = {
24
  text?: string
 
572
  </div>
573
  </div>
574
  <div className="mt-1.5 flex items-center justify-end gap-1 px-2 text-[10px] text-muted-foreground">
575
+ <AiFeatureToggle
576
+ storageKey="tans:self-critique"
577
+ label="Tự đánh giá câu trả lời"
578
+ icon={<Brain className="h-3.5 w-3.5" />}
579
+ defaultEnabled
580
+ onMessage="AI sẽ tự kiểm tra câu trả lời trước khi trả về để giảm sai sót."
581
+ offMessage="AI trả lời thẳng, không tự kiểm tra lại."
582
+ />
583
+ <AiFeatureToggle
584
+ storageKey="tans:auto-compact"
585
+ label="Tự nén context khi đầy"
586
+ icon={<FolderArchive className="h-3.5 w-3.5" />}
587
+ defaultEnabled
588
+ onMessage="Khi context gần đầy, các tin nhắn cũ sẽ được tóm tắt tự động để chừa chỗ cho phần mới."
589
+ offMessage="Context sẽ không được nén — bạn cần chủ động xóa hoặc tạo chat mới khi đầy."
590
+ />
591
  <span className="mx-1 h-3 w-px bg-border/60" aria-hidden />
592
  <Tooltip>
593
  <TooltipTrigger asChild>
 
631
  )
632
  }
633
 
634
+ function AiFeatureToggle({
635
+ storageKey,
636
+ label,
637
+ icon,
638
+ defaultEnabled = false,
639
+ onMessage,
640
+ offMessage,
641
+ }: {
642
+ storageKey: string
643
+ label: string
644
+ icon: React.ReactNode
645
+ defaultEnabled?: boolean
646
+ onMessage?: string
647
+ offMessage?: string
648
+ }) {
649
+ const [enabled, setEnabled] = useState(defaultEnabled)
650
  useEffect(() => {
651
  if (typeof window === "undefined") return
652
+ const raw = window.localStorage.getItem(storageKey)
653
+ if (raw === null) {
654
+ // First-time visitor — apply default + persist so it sticks.
655
+ setEnabled(defaultEnabled)
656
+ window.localStorage.setItem(storageKey, String(defaultEnabled))
657
+ } else {
658
+ setEnabled(raw === "true")
659
+ }
660
+ }, [storageKey, defaultEnabled])
661
  function toggle() {
662
  const next = !enabled
663
  setEnabled(next)
664
  if (typeof window !== "undefined") window.localStorage.setItem(storageKey, String(next))
665
+ if (next) {
666
+ toast.success(`${label}: Bật`, {
667
+ description: onMessage ?? "Đã bật tính năng.",
668
+ })
669
+ } else {
670
+ toast(`${label}: Tắt`, {
671
+ description: offMessage ?? "Đã tắt tính năng.",
672
+ })
673
+ }
674
  }
675
  return (
676
  <Tooltip>
package-lock.json CHANGED
@@ -40,6 +40,7 @@
40
  "rehype-katex": "^7.0.1",
41
  "remark-gfm": "^4.0.0",
42
  "remark-math": "^6.0.0",
 
43
  "tailwind-merge": "^2.5.4",
44
  "zod": "^3.23.8"
45
  },
@@ -8706,6 +8707,16 @@
8706
  "url": "https://github.com/sponsors/ljharb"
8707
  }
8708
  },
 
 
 
 
 
 
 
 
 
 
8709
  "node_modules/source-map-js": {
8710
  "version": "1.2.1",
8711
  "license": "BSD-3-Clause",
 
40
  "rehype-katex": "^7.0.1",
41
  "remark-gfm": "^4.0.0",
42
  "remark-math": "^6.0.0",
43
+ "sonner": "^2.0.7",
44
  "tailwind-merge": "^2.5.4",
45
  "zod": "^3.23.8"
46
  },
 
8707
  "url": "https://github.com/sponsors/ljharb"
8708
  }
8709
  },
8710
+ "node_modules/sonner": {
8711
+ "version": "2.0.7",
8712
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
8713
+ "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
8714
+ "license": "MIT",
8715
+ "peerDependencies": {
8716
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
8717
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
8718
+ }
8719
+ },
8720
  "node_modules/source-map-js": {
8721
  "version": "1.2.1",
8722
  "license": "BSD-3-Clause",
package.json CHANGED
@@ -41,6 +41,7 @@
41
  "rehype-katex": "^7.0.1",
42
  "remark-gfm": "^4.0.0",
43
  "remark-math": "^6.0.0",
 
44
  "tailwind-merge": "^2.5.4",
45
  "zod": "^3.23.8"
46
  },
 
41
  "rehype-katex": "^7.0.1",
42
  "remark-gfm": "^4.0.0",
43
  "remark-math": "^6.0.0",
44
+ "sonner": "^2.0.7",
45
  "tailwind-merge": "^2.5.4",
46
  "zod": "^3.23.8"
47
  },