File size: 8,284 Bytes
ddac6a2
 
 
 
 
 
 
fa4edd5
 
 
 
 
 
 
 
 
03bd69e
2316bca
 
 
 
 
 
 
03bd69e
fa4edd5
03bd69e
ddac6a2
 
 
 
ee61dec
 
fa4edd5
c1944cf
fa4edd5
 
ddac6a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee61dec
 
 
 
 
 
c1944cf
 
 
 
 
 
 
 
 
 
 
 
ee61dec
 
fa4edd5
 
 
 
 
 
 
 
 
ddac6a2
03bd69e
 
c1944cf
 
 
 
 
 
 
03bd69e
fa4edd5
03bd69e
 
 
 
fa4edd5
 
 
 
 
03bd69e
fa4edd5
03bd69e
 
 
fa4edd5
 
 
 
 
 
 
 
03bd69e
 
fa4edd5
 
 
 
 
ddac6a2
ee61dec
03bd69e
c1944cf
03bd69e
 
 
 
ee61dec
 
 
 
 
 
 
 
 
 
 
 
ddac6a2
ee61dec
 
03bd69e
ee61dec
03bd69e
ee61dec
fa4edd5
2316bca
ee61dec
03bd69e
ee61dec
 
 
 
 
03bd69e
 
 
 
ee61dec
 
 
 
 
ddac6a2
ee61dec
 
03bd69e
 
 
 
ee61dec
 
 
 
03bd69e
 
fa4edd5
 
 
 
 
 
 
 
2316bca
 
fa4edd5
 
ee61dec
 
 
 
 
ddac6a2
03bd69e
ddac6a2
 
 
 
03bd69e
ddac6a2
 
 
 
 
 
 
2316bca
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
import { useState } from 'react';
import ChatList from './components/ChatList';
import ChatWindow from './components/ChatWindow';
import Settings from './components/Settings';
import useLocalStorage from './hooks/useLocalStorage';

function App() {
  const [profiles, setProfiles] = useLocalStorage('profiles', [
    {
      id: 'default',
      name: 'Default Profile',
      apiEndpoint: '',
      apiKey: '',
      model: 'DeepSeek-R1'
    }
  ]);

  const [summarizationProfile, setSummarizationProfile] = useLocalStorage('summarizationProfile', {
    id: 'default-summarization-profile',
    name: 'Summarization Profile',
    apiEndpoint: '',
    apiKey: '',
    model: 'DeepSeek-R1'
  });

  const [activeProfileId, setActiveProfileId] = useLocalStorage('activeProfileId', 'default');

  const [chats, setChats] = useLocalStorage('chats', []);
  const [currentChatId, setCurrentChatId] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);
  const [showSettings, setShowSettings] = useState(false);
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
  const [showProfileDropdown, setShowProfileDropdown] = useState(false);
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

  const activeProfile = profiles.find(p => p.id === activeProfileId) || profiles[0];

  const createNewChat = () => {
    const newChat = {
      id: Date.now(),
      title: 'New Conversation',
      messages: []
    };
    setChats([...chats, newChat]);
    setCurrentChatId(newChat.id);
  };

  const deleteChat = (chatId) => {
    setChats(chats.filter(chat => chat.id !== chatId));
    if (currentChatId === chatId) {
      setCurrentChatId(null);
    }
  };

  const toggleSettings = () => {
    setShowSettings(!showSettings);
  };

  const toggleSidebar = () => {
    setSidebarCollapsed(!sidebarCollapsed);
    // 在移动设备上,如果侧边栏是展开的,点击收起按钮也应该关闭移动菜单
    if (window.innerWidth <= 768 && !sidebarCollapsed) {
      setMobileMenuOpen(false);
    }
  };

  const toggleMobileMenu = () => {
    setMobileMenuOpen(!mobileMenuOpen);
    // 如果侧边栏是收起的,则展开它
    if (sidebarCollapsed) {
      setSidebarCollapsed(false);
    }
  };

  const toggleProfileDropdown = () => {
    setShowProfileDropdown(!showProfileDropdown);
  };

  const handleProfileSelect = (profileId) => {
    setActiveProfileId(profileId);
    setShowProfileDropdown(false);
  };

  return (
    <div className="flex flex-col h-screen w-full overflow-hidden">
      <div className="flex justify-between items-center px-5 h-header border-b border-border bg-background">
        <div className="flex items-center gap-2">
          <button
            className="md:hidden bg-transparent border-0 text-base text-lightest-text cursor-pointer flex items-center justify-center z-10 w-8 h-8 hover:text-text"
            onClick={toggleMobileMenu}
          >

          </button>
          <h1 className="text-xl font-semibold text-text">Thinking Model Client</h1>
        </div>
        <div className="flex items-center gap-2.5">
          <div className="relative">
            <button
              className="py-1.5 px-3 bg-background border border-border rounded text-sm text-text cursor-pointer flex items-center gap-1.5"
              onClick={toggleProfileDropdown}
            >
              {activeProfile.name} ▼
            </button>
            {showProfileDropdown && (
              <div className="absolute top-full right-0 w-[200px] bg-background border border-border rounded shadow-md z-10 mt-1.5">
                {profiles.map(profile => (
                  <div
                    key={profile.id}
                    className={`p-3 cursor-pointer transition-colors duration-200 text-sm ${profile.id === activeProfileId ? 'bg-active font-medium' : 'hover:bg-hover'}`}
                    onClick={() => handleProfileSelect(profile.id)}
                  >
                    {profile.name}
                  </div>
                ))}
              </div>
            )}
          </div>
          <button
            className="py-1.5 px-3 bg-background border border-border rounded text-sm text-text cursor-pointer hover:bg-hover"
            onClick={toggleSettings}
          >
            Settings
          </button>
        </div>
      </div>

      <div className="flex flex-1 overflow-hidden">
        <div className={`sidebar ${sidebarCollapsed ? 'collapsed' : ''} ${mobileMenuOpen ? 'mobile-open' : ''}`}>
          <div className="flex justify-between items-center py-3 px-4 border-b border-border">
            <h2 className="text-sm font-semibold text-light-text m-0">Conversations</h2>
            <button
              className="bg-transparent border-0 text-base text-lightest-text cursor-pointer flex items-center justify-center z-10 w-6 h-6 hover:text-text"
              onClick={toggleSidebar}
            >
              {sidebarCollapsed ? '→' : '←'}
            </button>
          </div>
          <ChatList
            chats={chats}
            currentChat={chats.find(c => c.id === currentChatId)}
            onSelectChat={setCurrentChatId}
            onDeleteChat={deleteChat}
            onCreateNewChat={createNewChat}
            collapsed={sidebarCollapsed}
          />
        </div>

        <div className="flex-1 flex flex-col overflow-hidden bg-background">
          {currentChatId ? (
            <ChatWindow
              chat={chats.find(c => c.id === currentChatId)}
              profile={activeProfile}
              summarizationProfile={summarizationProfile}
              onUpdateChat={(updatedChat) => {
                setChats(chats.map(c =>
                  c.id === updatedChat.id ? updatedChat : c
                ));
              }}
            />
          ) : (
            <div className="flex flex-col items-center justify-center h-full p-5 text-center">
              <h2 className="text-2xl font-semibold mb-2.5 text-text">Welcome to Thinking Model Client</h2>
              <p className="text-light-text mb-5 text-sm">Start a new conversation or select an existing one.</p>
              <button className="py-2.5 px-5 bg-primary text-white border-none rounded cursor-pointer text-sm transition-colors duration-200 hover:bg-primary-hover" onClick={createNewChat}>
                Start New Conversation
              </button>
            </div>
          )}
        </div>
      </div>

      {showSettings && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[1000]">
          <div className="bg-background rounded-lg w-[600px] max-w-[90%] max-h-[90vh] overflow-y-auto relative p-6 shadow-md">
            <button
              className="absolute top-4 right-4 bg-transparent border-none text-xl cursor-pointer text-lightest-text w-6 h-6 flex items-center justify-center rounded hover:bg-hover hover:text-text"
              onClick={toggleSettings}
            >
              ×
            </button>
              <Settings
              profiles={profiles}
              activeProfileId={activeProfileId}
              onSaveProfiles={(newProfiles) => {
                setProfiles(newProfiles);
                // Ensure the active profile still exists, otherwise select the first one
                if (!newProfiles.some(p => p.id === activeProfileId)) {
                  setActiveProfileId(newProfiles[0]?.id || null);
                }
              }}
              onSaveSummarizationProfile={setSummarizationProfile}
              summarizationProfile={summarizationProfile}
              onChangeActiveProfile={setActiveProfileId}
              onCloseSettings={() => setShowSettings(false)}
            />
          </div>
        </div>
      )}

      {error && (
        <div className="fixed bottom-5 left-1/2 -translate-x-1/2 bg-red-100 text-red-600 py-2.5 px-5 rounded text-sm shadow-md z-[1000]">
          Error: {error}
        </div>
      )}
      {loading && (
        <div className="fixed bottom-5 left-1/2 -translate-x-1/2 bg-blue-50 text-blue-700 py-2.5 px-5 rounded text-sm shadow-md z-[1000]">
          Loading...
        </div>
      )}
    </div>
  );
}

export default App;