File size: 12,332 Bytes
68c2137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
import { useState, useEffect } from 'react';
import { Header } from './components/Header';
import { Sidebar } from './components/Sidebar';
import { ChatArea } from './components/ChatArea';
import { ChatInput } from './components/ChatInput';
import { SettingsModal } from './components/SettingsModal';
import { PDFPreviewModal } from './components/PDFPreviewModal';
import { DashboardModal } from './components/DashboardModal';
import type { Conversation, Message, ModelParams, ModelStats } from './types/chat';
import { streamChatMessage, fetchModelInfo, fetchSystemPrompt } from './services/api';

const DEFAULT_PARAMS: ModelParams = {
  temperature: 0.0,
  topK: 1,
  topP: 1.0,
  maxTokens: 128,
  repetitionPenalty: 1.05,
  streaming: true
};

const INITIAL_CONVERSATION: Conversation = {
  id: 'conv-1',
  title: 'LawSLM Assistant Welcome Chat',
  pinned: true,
  updatedAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
  messages: [
    {
      id: 'msg-1',
      role: 'assistant',
      content: 'Hello! I am **LawSLM**, a Small Language Model built completely from scratch by **Amit Kumar**. I can assist you with legal information, programming, report generation, and general AI tasks. How can I help you today?',
      timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
    }
  ]
};

export function App() {
  const [darkMode, setDarkMode] = useState<boolean>(() => {
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  });
  const [sidebarOpen, setSidebarOpen] = useState<boolean>(true);
  const [conversations, setConversations] = useState<Conversation[]>([INITIAL_CONVERSATION]);
  const [activeId, setActiveId] = useState<string>('conv-1');
  const [params, setParams] = useState<ModelParams>(DEFAULT_PARAMS);
  const [systemPrompt, setSystemPrompt] = useState<string>('');
  const [modelStats, setModelStats] = useState<ModelStats>({
    status: 'online',
    vocabSize: 2000,
    dModel: 128,
    nLayers: 2,
    nHeads: 4,
    totalParams: '624.38K',
    trainableParams: '624.38K',
    activeDevice: 'CPU',
    ramUsage: '128 MB',
    checkpointLoaded: 'checkpoints/best_model.pt'
  });

  const [isGenerating, setIsGenerating] = useState<boolean>(false);
  const [settingsOpen, setSettingsOpen] = useState<boolean>(false);
  const [dashboardOpen, setDashboardOpen] = useState<boolean>(false);
  const [pdfModalData, setPdfModalData] = useState<Message['pdfPreview'] | null>(null);
  const [systemPromptOpen, setSystemPromptOpen] = useState<boolean>(false);

  // Sync dark mode class with body
  useEffect(() => {
    if (darkMode) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }, [darkMode]);

  // Load API Stats & System Prompt on mount
  useEffect(() => {
    fetchModelInfo().then(stats => stats && setModelStats(stats));
    fetchSystemPrompt().then(prompt => setSystemPrompt(prompt));
  }, []);

  const activeConv = conversations.find(c => c.id === activeId) || conversations[0];

  const handleNewChat = () => {
    const newConv: Conversation = {
      id: `conv-${Date.now()}`,
      title: 'New Conversation',
      pinned: false,
      updatedAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
      messages: []
    };
    setConversations([newConv, ...conversations]);
    setActiveId(newConv.id);
  };

  const handleDeleteChat = (id: string) => {
    const updated = conversations.filter(c => c.id !== id);
    if (updated.length === 0) {
      handleNewChat();
    } else {
      setConversations(updated);
      if (activeId === id) setActiveId(updated[0].id);
    }
  };

  const handleRenameChat = (id: string, newTitle: string) => {
    setConversations(conversations.map(c => c.id === id ? { ...c, title: newTitle } : c));
  };

  const handlePinChat = (id: string) => {
    setConversations(conversations.map(c => c.id === id ? { ...c, pinned: !c.pinned } : c));
  };

  const handleSendMessage = async (text: string, attachments?: any[]) => {
    if (!text.trim() && (!attachments || attachments.length === 0)) return;

    const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const userMsg: Message = {
      id: `msg-${Date.now()}`,
      role: 'user',
      content: text,
      timestamp,
      attachments
    };

    const shouldRename = activeConv.messages.length === 0;
    const updatedTitle = shouldRename ? (text.length > 25 ? text.substring(0, 25) + '...' : text) : activeConv.title;

    setIsGenerating(true);

    const assistantMsgId = `msg-${Date.now() + 1}`;
    const startTime = Date.now();

    const isPDFRequest = text.toLowerCase().includes('pdf') || text.toLowerCase().includes('report') || text.toLowerCase().includes('affidavit') || text.toLowerCase().includes('notice');
    
    let pdfPreviewData: Message['pdfPreview'] | undefined = undefined;
    if (isPDFRequest) {
      pdfPreviewData = {
        title: "LEGAL NOTICE & FORMAL DEMAND REPORT",
        summary: "Formal Legal Notice issued under statutory provisions demanding compliance and legal clarification.",
        sections: [
          { heading: "PARTIES & BACKGROUND", body: "This legal notice is served on behalf of the Claimant regarding unresolved contractual obligations." },
          { heading: "STATUTORY GROUNDS & SECTIONS", body: "Under relevant statutory codes, failure to comply within 15 days will result in formal court litigation." },
          { heading: "DEMAND & REMEDIES", body: "The Addressee is hereby directed to remit the outstanding sum and provide written confirmation." }
        ]
      };
    }

    const placeholderAssistantMsg: Message = {
      id: assistantMsgId,
      role: 'assistant',
      content: '',
      timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
      isStreaming: true,
      pdfPreview: pdfPreviewData
    };

    // SINGLE atomic state update — adds userMsg + placeholder assistant msg together
    setConversations(prev => prev.map(c => c.id === activeId ? {
      ...c,
      title: updatedTitle,
      updatedAt: timestamp,
      messages: [...c.messages, userMsg, placeholderAssistantMsg]
    } : c));

    try {
      let currentPdfMeta: Message['pdfPreview'] = pdfPreviewData;
      const responseText = await streamChatMessage(
        text,
        params,
        (chunkText: string) => {
          setConversations(prev => prev.map(c => {
            if (c.id !== activeId) return c;
            const msgs: Message[] = c.messages.map((m): Message => {
              if (m.id === assistantMsgId) {
                return { ...m, content: chunkText };
              }
              return m;
            });
            return { ...c, messages: msgs };
          }));
        },
        (meta: { hasPdf?: boolean; pdfMeta?: any }) => {
          if (meta.hasPdf && meta.pdfMeta) {
            currentPdfMeta = {
              title: meta.pdfMeta.title || "FORMAL DOCUMENT REPORT",
              summary: meta.pdfMeta.summary || "Generated by LawSLM",
              sections: [
                { heading: "EXECUTIVE SUMMARY", body: meta.pdfMeta.summary || "Official Document Summary" },
                { heading: "FULL DETAILS", body: meta.pdfMeta.content || "Report Details" }
              ]
            };
          }
        }
      );

      const endTime = Date.now();

      setConversations(prev => prev.map(c => {
        if (c.id !== activeId) return c;
        const msgs: Message[] = c.messages.map((m): Message => {
          if (m.id === assistantMsgId) {
            return {
              ...m,
              content: responseText || m.content,
              isStreaming: false,
              responseTimeMs: endTime - startTime,
              pdfPreview: currentPdfMeta
            };
          }
          return m;
        });
        return { ...c, messages: msgs };
      }));
    } catch (err) {
      console.error(err);
    } finally {
      setIsGenerating(false);
    }
  };

  const handleRegenerate = () => {
    if (activeConv.messages.length < 2) return;
    const lastUserMsg = [...activeConv.messages].reverse().find(m => m.role === 'user');
    if (lastUserMsg) {
      handleSendMessage(lastUserMsg.content);
    }
  };

  const handleEditMessage = (id: string, newText: string) => {
    setConversations(prev => prev.map(c => {
      if (c.id !== activeId) return c;
      const msgs: Message[] = c.messages.map((m): Message => {
        if (m.id === id) {
          return { ...m, content: newText };
        }
        return m;
      });
      return { ...c, messages: msgs };
    }));
    handleSendMessage(newText);
  };

  const handleDeleteMessage = (id: string) => {
    setConversations(prev => prev.map(c => {
      if (c.id !== activeId) return c;
      return { ...c, messages: c.messages.filter(m => m.id !== id) };
    }));
  };

  return (
    <div className="flex h-screen w-screen overflow-hidden bg-slate-900 text-slate-100">
      {/* Sidebar Navigation */}
      <Sidebar
        conversations={conversations}
        activeId={activeId}
        onSelectConversation={setActiveId}
        onNewChat={handleNewChat}
        onDeleteChat={handleDeleteChat}
        onRenameChat={handleRenameChat}
        onPinChat={handlePinChat}
        onOpenSettings={() => setSettingsOpen(true)}
        onOpenDashboard={() => setDashboardOpen(true)}
        open={sidebarOpen}
        setOpen={setSidebarOpen}
      />

      {/* Main Workspace Column */}
      <div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
        <Header
          darkMode={darkMode}
          setDarkMode={setDarkMode}
          onOpenSettings={() => setSettingsOpen(true)}
          onOpenDashboard={() => setDashboardOpen(true)}
          onOpenSystemPrompt={() => setSystemPromptOpen(true)}
          modelStats={modelStats}
          sidebarOpen={sidebarOpen}
          setSidebarOpen={setSidebarOpen}
        />

        {/* Central Chat View */}
        <ChatArea
          messages={activeConv.messages}
          onSendMessage={handleSendMessage}
          onRegenerate={handleRegenerate}
          onOpenPDFPreview={pdf => setPdfModalData(pdf)}
          onEditMessage={handleEditMessage}
          onDeleteMessage={handleDeleteMessage}
          isGenerating={isGenerating}
        />

        {/* Chat Input Toolbar */}
        <ChatInput
          onSendMessage={handleSendMessage}
          isGenerating={isGenerating}
          onStopGeneration={() => setIsGenerating(false)}
        />
      </div>

      {/* Settings Modal */}
      <SettingsModal
        isOpen={settingsOpen}
        onClose={() => setSettingsOpen(false)}
        params={params}
        onUpdateParams={setParams}
        systemPrompt={systemPrompt}
        onUpdateSystemPrompt={setSystemPrompt}
      />

      {/* PDF Document Preview Modal */}
      <PDFPreviewModal
        isOpen={!!pdfModalData}
        onClose={() => setPdfModalData(null)}
        pdfData={pdfModalData || undefined}
      />

      {/* Model Dashboard Modal */}
      <DashboardModal
        isOpen={dashboardOpen}
        onClose={() => setDashboardOpen(false)}
        stats={modelStats}
      />

      {/* System Prompt View Drawer */}
      {systemPromptOpen && (
        <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
          <div className="w-full max-w-xl glass-panel rounded-2xl p-6 shadow-2xl space-y-4">
            <div className="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-3">
              <h3 className="font-bold text-sm text-slate-900 dark:text-white">LawSLM System Prompt & Safety Guidelines</h3>
              <button onClick={() => setSystemPromptOpen(false)} className="text-slate-400 hover:text-slate-600">×</button>
            </div>
            <pre className="text-xs font-mono bg-slate-100 dark:bg-darkbg-900 p-4 rounded-xl max-h-96 overflow-y-auto whitespace-pre-wrap leading-relaxed text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-800">
              {systemPrompt}
            </pre>
          </div>
        </div>
      )}
    </div>
  );
}

export default App;