Humbl3m33 commited on
Commit
f5d1968
·
verified ·
1 Parent(s): 7755ea4

Upload 3 files

Browse files
Files changed (3) hide show
  1. use-chat-history.ts +105 -0
  2. use-chat.ts +200 -0
  3. use-toast.ts +194 -0
use-chat-history.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useCallback } from "react"
4
+ import { useToast } from "@/hooks/use-toast"
5
+
6
+ export function useChatHistory() {
7
+ const { toast } = useToast()
8
+
9
+ const exportHistory = useCallback(async () => {
10
+ try {
11
+ const response = await fetch("/api/export/json")
12
+ const data = await response.json()
13
+
14
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
15
+ type: "application/json",
16
+ })
17
+ const url = URL.createObjectURL(blob)
18
+ const a = document.createElement("a")
19
+ a.href = url
20
+ a.download = `chat-history-${new Date().toISOString().split("T")[0]}.json`
21
+ a.click()
22
+ URL.revokeObjectURL(url)
23
+
24
+ toast({
25
+ title: "Export successful",
26
+ description: "Chat history exported to JSON file",
27
+ })
28
+ } catch (error) {
29
+ toast({
30
+ title: "Export failed",
31
+ description: "Could not export chat history",
32
+ variant: "destructive",
33
+ })
34
+ }
35
+ }, [toast])
36
+
37
+ const exportHTML = useCallback(async () => {
38
+ try {
39
+ const response = await fetch("/api/export/html")
40
+ const blob = await response.blob()
41
+
42
+ const url = URL.createObjectURL(blob)
43
+ const a = document.createElement("a")
44
+ a.href = url
45
+ a.download = `chat-export-${new Date().toISOString().split("T")[0]}.html`
46
+ a.click()
47
+ URL.revokeObjectURL(url)
48
+
49
+ toast({
50
+ title: "HTML export successful",
51
+ description: "Chat history exported to HTML file",
52
+ })
53
+ } catch (error) {
54
+ toast({
55
+ title: "Export failed",
56
+ description: "Could not export to HTML",
57
+ variant: "destructive",
58
+ })
59
+ }
60
+ }, [toast])
61
+
62
+ const importHistory = useCallback(() => {
63
+ const input = document.createElement("input")
64
+ input.type = "file"
65
+ input.accept = ".json"
66
+ input.onchange = async (e) => {
67
+ const file = (e.target as HTMLInputElement).files?.[0]
68
+ if (!file) return
69
+
70
+ try {
71
+ const text = await file.text()
72
+ const data = JSON.parse(text)
73
+
74
+ const response = await fetch("/api/import", {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify(data),
78
+ })
79
+
80
+ if (response.ok) {
81
+ toast({
82
+ title: "Import successful",
83
+ description: "Chat history imported successfully",
84
+ })
85
+ window.location.reload() // Refresh to show imported data
86
+ } else {
87
+ throw new Error("Import failed")
88
+ }
89
+ } catch (error) {
90
+ toast({
91
+ title: "Import failed",
92
+ description: "Could not import chat history",
93
+ variant: "destructive",
94
+ })
95
+ }
96
+ }
97
+ input.click()
98
+ }, [toast])
99
+
100
+ return {
101
+ exportHistory,
102
+ exportHTML,
103
+ importHistory,
104
+ }
105
+ }
use-chat.ts ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useCallback, useEffect } from "react"
4
+ import type { Message, AIProvider } from "@/types/chat"
5
+ import { useToast } from "@/hooks/use-toast"
6
+
7
+ export function useChat(provider: AIProvider) {
8
+ const [messages, setMessages] = useState<Message[]>([])
9
+ const [isLoading, setIsLoading] = useState(false)
10
+ const [suggestions, setSuggestions] = useState<string[]>([
11
+ "Explain this concept",
12
+ "Give me examples",
13
+ "Summarize the key points",
14
+ "What are the pros and cons?",
15
+ "How does this work?",
16
+ ])
17
+ const { toast } = useToast()
18
+
19
+ useEffect(() => {
20
+ loadChatHistory()
21
+ }, [])
22
+
23
+ const loadChatHistory = useCallback(async () => {
24
+ try {
25
+ const response = await fetch("/api/chat/history")
26
+ if (response.ok) {
27
+ const data = await response.json()
28
+ setMessages(data.messages || [])
29
+ }
30
+ } catch (error) {
31
+ console.error("Failed to load chat history:", error)
32
+ }
33
+ }, [])
34
+
35
+ const sendMessage = useCallback(
36
+ async (content: string, file?: File) => {
37
+ if (!content.trim() && !file) return
38
+
39
+ const userMessage: Message = {
40
+ id: Date.now().toString(),
41
+ role: "user",
42
+ content: content || `[File: ${file?.name}]`,
43
+ timestamp: new Date(),
44
+ file: file
45
+ ? {
46
+ name: file.name,
47
+ type: file.type,
48
+ content: await file.text().catch(() => "[Binary file]"),
49
+ }
50
+ : undefined,
51
+ }
52
+
53
+ setMessages((prev) => [...prev, userMessage])
54
+ setIsLoading(true)
55
+
56
+ try {
57
+ const response = await fetch("/api/chat", {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ message: content,
62
+ provider,
63
+ file: userMessage.file,
64
+ history: messages.slice(-10),
65
+ }),
66
+ })
67
+
68
+ if (!response.ok) {
69
+ throw new Error(`HTTP error! status: ${response.status}`)
70
+ }
71
+
72
+ const data = await response.json()
73
+
74
+ const assistantMessage: Message = {
75
+ id: (Date.now() + 1).toString(),
76
+ role: "assistant",
77
+ content: data.content,
78
+ provider,
79
+ timestamp: new Date(),
80
+ }
81
+
82
+ setMessages((prev) => [...prev, assistantMessage])
83
+
84
+ setSuggestions([
85
+ "Explain this further",
86
+ "Give me a different perspective",
87
+ "What's the next step?",
88
+ "Can you elaborate?",
89
+ "Show me an example",
90
+ ])
91
+ } catch (error) {
92
+ console.error("Chat error:", error)
93
+ toast({
94
+ title: "Error",
95
+ description: "Failed to send message. Please try again.",
96
+ variant: "destructive",
97
+ })
98
+ } finally {
99
+ setIsLoading(false)
100
+ }
101
+ },
102
+ [provider, messages, toast],
103
+ )
104
+
105
+ const clearChat = useCallback(async () => {
106
+ try {
107
+ await fetch("/api/chat/clear", { method: "POST" })
108
+ setMessages([])
109
+ setSuggestions([
110
+ "Explain this concept",
111
+ "Give me examples",
112
+ "Summarize the key points",
113
+ "What are the pros and cons?",
114
+ "How does this work?",
115
+ ])
116
+ toast({ title: "Chat cleared successfully" })
117
+ } catch (error) {
118
+ toast({
119
+ title: "Error",
120
+ description: "Failed to clear chat",
121
+ variant: "destructive",
122
+ })
123
+ }
124
+ }, [toast])
125
+
126
+ const uploadHistory = useCallback(
127
+ (file: File) => {
128
+ const reader = new FileReader()
129
+ reader.onload = () => {
130
+ try {
131
+ const data = JSON.parse(reader.result as string)
132
+ setMessages(data.messages || data || [])
133
+ toast({ title: "Chat history uploaded successfully" })
134
+ } catch (error) {
135
+ toast({
136
+ title: "Error",
137
+ description: "Invalid file format",
138
+ variant: "destructive",
139
+ })
140
+ }
141
+ }
142
+ reader.readAsText(file)
143
+ },
144
+ [toast],
145
+ )
146
+
147
+ const downloadHistory = useCallback(() => {
148
+ const data = { messages, exportDate: new Date().toISOString() }
149
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" })
150
+ const link = document.createElement("a")
151
+ link.href = URL.createObjectURL(blob)
152
+ link.download = `chat-history-${new Date().toISOString().split("T")[0]}.json`
153
+ link.click()
154
+ toast({ title: "Chat history downloaded" })
155
+ }, [messages, toast])
156
+
157
+ const exportHTML = useCallback(async () => {
158
+ try {
159
+ const response = await fetch("/api/export/html", {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ messages }),
163
+ })
164
+
165
+ if (response.ok) {
166
+ const blob = await response.blob()
167
+ const link = document.createElement("a")
168
+ link.href = URL.createObjectURL(blob)
169
+ link.download = `chat-export-${new Date().toISOString().split("T")[0]}.html`
170
+ link.click()
171
+ toast({ title: "HTML export downloaded" })
172
+ }
173
+ } catch (error) {
174
+ toast({
175
+ title: "Error",
176
+ description: "Failed to export HTML",
177
+ variant: "destructive",
178
+ })
179
+ }
180
+ }, [messages, toast])
181
+
182
+ const copyJSON = useCallback(() => {
183
+ const data = { messages, exportDate: new Date().toISOString() }
184
+ navigator.clipboard.writeText(JSON.stringify(data, null, 2))
185
+ toast({ title: "Chat JSON copied to clipboard" })
186
+ }, [messages, toast])
187
+
188
+ return {
189
+ messages,
190
+ isLoading,
191
+ sendMessage,
192
+ suggestions,
193
+ clearChat,
194
+ uploadHistory,
195
+ downloadHistory,
196
+ exportHTML,
197
+ copyJSON,
198
+ loadChatHistory,
199
+ }
200
+ }
use-toast.ts ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ // Inspired by react-hot-toast library
4
+ import * as React from "react"
5
+
6
+ import type {
7
+ ToastActionElement,
8
+ ToastProps,
9
+ } from "@/components/ui/toast"
10
+
11
+ const TOAST_LIMIT = 1
12
+ const TOAST_REMOVE_DELAY = 1000000
13
+
14
+ type ToasterToast = ToastProps & {
15
+ id: string
16
+ title?: React.ReactNode
17
+ description?: React.ReactNode
18
+ action?: ToastActionElement
19
+ }
20
+
21
+ const actionTypes = {
22
+ ADD_TOAST: "ADD_TOAST",
23
+ UPDATE_TOAST: "UPDATE_TOAST",
24
+ DISMISS_TOAST: "DISMISS_TOAST",
25
+ REMOVE_TOAST: "REMOVE_TOAST",
26
+ } as const
27
+
28
+ let count = 0
29
+
30
+ function genId() {
31
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
32
+ return count.toString()
33
+ }
34
+
35
+ type ActionType = typeof actionTypes
36
+
37
+ type Action =
38
+ | {
39
+ type: ActionType["ADD_TOAST"]
40
+ toast: ToasterToast
41
+ }
42
+ | {
43
+ type: ActionType["UPDATE_TOAST"]
44
+ toast: Partial<ToasterToast>
45
+ }
46
+ | {
47
+ type: ActionType["DISMISS_TOAST"]
48
+ toastId?: ToasterToast["id"]
49
+ }
50
+ | {
51
+ type: ActionType["REMOVE_TOAST"]
52
+ toastId?: ToasterToast["id"]
53
+ }
54
+
55
+ interface State {
56
+ toasts: ToasterToast[]
57
+ }
58
+
59
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
60
+
61
+ const addToRemoveQueue = (toastId: string) => {
62
+ if (toastTimeouts.has(toastId)) {
63
+ return
64
+ }
65
+
66
+ const timeout = setTimeout(() => {
67
+ toastTimeouts.delete(toastId)
68
+ dispatch({
69
+ type: "REMOVE_TOAST",
70
+ toastId: toastId,
71
+ })
72
+ }, TOAST_REMOVE_DELAY)
73
+
74
+ toastTimeouts.set(toastId, timeout)
75
+ }
76
+
77
+ export const reducer = (state: State, action: Action): State => {
78
+ switch (action.type) {
79
+ case "ADD_TOAST":
80
+ return {
81
+ ...state,
82
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83
+ }
84
+
85
+ case "UPDATE_TOAST":
86
+ return {
87
+ ...state,
88
+ toasts: state.toasts.map((t) =>
89
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
90
+ ),
91
+ }
92
+
93
+ case "DISMISS_TOAST": {
94
+ const { toastId } = action
95
+
96
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
97
+ // but I'll keep it here for simplicity
98
+ if (toastId) {
99
+ addToRemoveQueue(toastId)
100
+ } else {
101
+ state.toasts.forEach((toast) => {
102
+ addToRemoveQueue(toast.id)
103
+ })
104
+ }
105
+
106
+ return {
107
+ ...state,
108
+ toasts: state.toasts.map((t) =>
109
+ t.id === toastId || toastId === undefined
110
+ ? {
111
+ ...t,
112
+ open: false,
113
+ }
114
+ : t
115
+ ),
116
+ }
117
+ }
118
+ case "REMOVE_TOAST":
119
+ if (action.toastId === undefined) {
120
+ return {
121
+ ...state,
122
+ toasts: [],
123
+ }
124
+ }
125
+ return {
126
+ ...state,
127
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
128
+ }
129
+ }
130
+ }
131
+
132
+ const listeners: Array<(state: State) => void> = []
133
+
134
+ let memoryState: State = { toasts: [] }
135
+
136
+ function dispatch(action: Action) {
137
+ memoryState = reducer(memoryState, action)
138
+ listeners.forEach((listener) => {
139
+ listener(memoryState)
140
+ })
141
+ }
142
+
143
+ type Toast = Omit<ToasterToast, "id">
144
+
145
+ function toast({ ...props }: Toast) {
146
+ const id = genId()
147
+
148
+ const update = (props: ToasterToast) =>
149
+ dispatch({
150
+ type: "UPDATE_TOAST",
151
+ toast: { ...props, id },
152
+ })
153
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154
+
155
+ dispatch({
156
+ type: "ADD_TOAST",
157
+ toast: {
158
+ ...props,
159
+ id,
160
+ open: true,
161
+ onOpenChange: (open) => {
162
+ if (!open) dismiss()
163
+ },
164
+ },
165
+ })
166
+
167
+ return {
168
+ id: id,
169
+ dismiss,
170
+ update,
171
+ }
172
+ }
173
+
174
+ function useToast() {
175
+ const [state, setState] = React.useState<State>(memoryState)
176
+
177
+ React.useEffect(() => {
178
+ listeners.push(setState)
179
+ return () => {
180
+ const index = listeners.indexOf(setState)
181
+ if (index > -1) {
182
+ listeners.splice(index, 1)
183
+ }
184
+ }
185
+ }, [state])
186
+
187
+ return {
188
+ ...state,
189
+ toast,
190
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191
+ }
192
+ }
193
+
194
+ export { useToast, toast }