moelove commited on
Commit
c1944cf
·
1 Parent(s): 03bd69e

Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>

index.html CHANGED
@@ -2,8 +2,12 @@
2
  <html lang="zh-CN">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Thinking Model Client</title>
 
 
 
 
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
@@ -12,4 +16,4 @@
12
  <div id="root"></div>
13
  <script type="module" src="/src/main.jsx"></script>
14
  </body>
15
- </html>
 
2
  <html lang="zh-CN">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
  <title>Thinking Model Client</title>
7
+ <meta name="description" content="A client for interacting with thinking models" />
8
+ <meta name="theme-color" content="#3e6ae1" />
9
+ <link rel="manifest" href="/manifest.json" />
10
+ <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
11
  <link rel="preconnect" href="https://fonts.googleapis.com">
12
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
 
16
  <div id="root"></div>
17
  <script type="module" src="/src/main.jsx"></script>
18
  </body>
19
+ </html>
public/icons/icon-192x192.png ADDED
public/icons/icon-512x512.png ADDED
public/icons/icon-512x512.svg ADDED
public/manifest.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Thinking Model Client",
3
+ "short_name": "Thinking Model",
4
+ "description": "A client for interacting with thinking models",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#3e6ae1",
9
+ "icons": [
10
+ {
11
+ "src": "icons/icon-72x72.png",
12
+ "sizes": "72x72",
13
+ "type": "image/png",
14
+ "purpose": "any maskable"
15
+ },
16
+ {
17
+ "src": "icons/icon-96x96.png",
18
+ "sizes": "96x96",
19
+ "type": "image/png",
20
+ "purpose": "any maskable"
21
+ },
22
+ {
23
+ "src": "icons/icon-128x128.png",
24
+ "sizes": "128x128",
25
+ "type": "image/png",
26
+ "purpose": "any maskable"
27
+ },
28
+ {
29
+ "src": "icons/icon-144x144.png",
30
+ "sizes": "144x144",
31
+ "type": "image/png",
32
+ "purpose": "any maskable"
33
+ },
34
+ {
35
+ "src": "icons/icon-152x152.png",
36
+ "sizes": "152x152",
37
+ "type": "image/png",
38
+ "purpose": "any maskable"
39
+ },
40
+ {
41
+ "src": "icons/icon-192x192.png",
42
+ "sizes": "192x192",
43
+ "type": "image/png",
44
+ "purpose": "any maskable"
45
+ },
46
+ {
47
+ "src": "icons/icon-384x384.png",
48
+ "sizes": "384x384",
49
+ "type": "image/png",
50
+ "purpose": "any maskable"
51
+ },
52
+ {
53
+ "src": "icons/icon-512x512.png",
54
+ "sizes": "512x512",
55
+ "type": "image/png",
56
+ "purpose": "any maskable"
57
+ }
58
+ ]
59
+ }
public/sw.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Service Worker for Thinking Model Client PWA
2
+
3
+ const CACHE_NAME = 'thinking-model-cache-v1';
4
+ const urlsToCache = [
5
+ '/',
6
+ '/index.html',
7
+ '/manifest.json',
8
+ '/icons/icon-192x192.png',
9
+ '/icons/icon-512x512.png'
10
+ ];
11
+
12
+ // Install event - cache basic assets
13
+ self.addEventListener('install', event => {
14
+ event.waitUntil(
15
+ caches.open(CACHE_NAME)
16
+ .then(cache => {
17
+ console.log('Opened cache');
18
+ return cache.addAll(urlsToCache);
19
+ })
20
+ );
21
+ });
22
+
23
+ // Activate event - clean up old caches
24
+ self.addEventListener('activate', event => {
25
+ const cacheWhitelist = [CACHE_NAME];
26
+ event.waitUntil(
27
+ caches.keys().then(cacheNames => {
28
+ return Promise.all(
29
+ cacheNames.map(cacheName => {
30
+ if (cacheWhitelist.indexOf(cacheName) === -1) {
31
+ return caches.delete(cacheName);
32
+ }
33
+ })
34
+ );
35
+ })
36
+ );
37
+ });
38
+
39
+ // Fetch event - serve from cache if available, otherwise fetch from network
40
+ self.addEventListener('fetch', event => {
41
+ event.respondWith(
42
+ caches.match(event.request)
43
+ .then(response => {
44
+ // Cache hit - return response
45
+ if (response) {
46
+ return response;
47
+ }
48
+ return fetch(event.request).then(
49
+ response => {
50
+ // Check if we received a valid response
51
+ if (!response || response.status !== 200 || response.type !== 'basic') {
52
+ return response;
53
+ }
54
+
55
+ // Clone the response
56
+ const responseToCache = response.clone();
57
+
58
+ caches.open(CACHE_NAME)
59
+ .then(cache => {
60
+ // Don't cache API requests
61
+ if (!event.request.url.includes('/api/')) {
62
+ cache.put(event.request, responseToCache);
63
+ }
64
+ });
65
+
66
+ return response;
67
+ }
68
+ );
69
+ })
70
+ );
71
+ });
scripts/generate-icons.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This script can be used to generate different sized icons from the SVG source
2
+ // You would need to install sharp: npm install sharp
3
+ // Then run: node scripts/generate-icons.js
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const sharp = require('sharp');
8
+
9
+ const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
10
+ const svgPath = path.join(__dirname, '../public/icons/icon-512x512.svg');
11
+ const outputDir = path.join(__dirname, '../public/icons');
12
+
13
+ async function generateIcons() {
14
+ try {
15
+ // Make sure the output directory exists
16
+ if (!fs.existsSync(outputDir)) {
17
+ fs.mkdirSync(outputDir, { recursive: true });
18
+ }
19
+
20
+ // Read the SVG file
21
+ const svgBuffer = fs.readFileSync(svgPath);
22
+
23
+ // Generate each size
24
+ for (const size of sizes) {
25
+ const outputPath = path.join(outputDir, `icon-${size}x${size}.png`);
26
+
27
+ await sharp(svgBuffer)
28
+ .resize(size, size)
29
+ .png()
30
+ .toFile(outputPath);
31
+
32
+ console.log(`Generated: ${outputPath}`);
33
+ }
34
+
35
+ console.log('All icons generated successfully!');
36
+ } catch (error) {
37
+ console.error('Error generating icons:', error);
38
+ }
39
+ }
40
+
41
+ generateIcons();
src/App.jsx CHANGED
@@ -32,6 +32,7 @@ function App() {
32
  const [showSettings, setShowSettings] = useState(false);
33
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
34
  const [showProfileDropdown, setShowProfileDropdown] = useState(false);
 
35
 
36
  const activeProfile = profiles.find(p => p.id === activeProfileId) || profiles[0];
37
 
@@ -58,6 +59,18 @@ function App() {
58
 
59
  const toggleSidebar = () => {
60
  setSidebarCollapsed(!sidebarCollapsed);
 
 
 
 
 
 
 
 
 
 
 
 
61
  };
62
 
63
  const toggleProfileDropdown = () => {
@@ -72,7 +85,13 @@ function App() {
72
  return (
73
  <div className="flex flex-col h-screen w-full overflow-hidden">
74
  <div className="flex justify-between items-center px-5 h-header border-b border-border bg-background">
75
- <div className="flex items-center">
 
 
 
 
 
 
76
  <h1 className="text-xl font-semibold text-text">Thinking Model Client</h1>
77
  </div>
78
  <div className="flex items-center gap-2.5">
@@ -107,7 +126,7 @@ function App() {
107
  </div>
108
 
109
  <div className="flex flex-1 overflow-hidden">
110
- <div className={`w-sidebar border-r border-border flex flex-col transition-[width] duration-300 ease-in-out bg-background ${sidebarCollapsed ? 'w-[50px] overflow-hidden' : ''}`}>
111
  <div className="flex justify-between items-center py-3 px-4 border-b border-border">
112
  <h2 className="text-sm font-semibold text-light-text m-0">Conversations</h2>
113
  <button
 
32
  const [showSettings, setShowSettings] = useState(false);
33
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
34
  const [showProfileDropdown, setShowProfileDropdown] = useState(false);
35
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
36
 
37
  const activeProfile = profiles.find(p => p.id === activeProfileId) || profiles[0];
38
 
 
59
 
60
  const toggleSidebar = () => {
61
  setSidebarCollapsed(!sidebarCollapsed);
62
+ // 在移动设备上,如果侧边栏是展开的,点击收起按钮也应该关闭移动菜单
63
+ if (window.innerWidth <= 768 && !sidebarCollapsed) {
64
+ setMobileMenuOpen(false);
65
+ }
66
+ };
67
+
68
+ const toggleMobileMenu = () => {
69
+ setMobileMenuOpen(!mobileMenuOpen);
70
+ // 如果侧边栏是收起的,则展开它
71
+ if (sidebarCollapsed) {
72
+ setSidebarCollapsed(false);
73
+ }
74
  };
75
 
76
  const toggleProfileDropdown = () => {
 
85
  return (
86
  <div className="flex flex-col h-screen w-full overflow-hidden">
87
  <div className="flex justify-between items-center px-5 h-header border-b border-border bg-background">
88
+ <div className="flex items-center gap-2">
89
+ <button
90
+ 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"
91
+ onClick={toggleMobileMenu}
92
+ >
93
+
94
+ </button>
95
  <h1 className="text-xl font-semibold text-text">Thinking Model Client</h1>
96
  </div>
97
  <div className="flex items-center gap-2.5">
 
126
  </div>
127
 
128
  <div className="flex flex-1 overflow-hidden">
129
+ <div className={`sidebar ${sidebarCollapsed ? 'collapsed' : ''} ${mobileMenuOpen ? 'mobile-open' : ''}`}>
130
  <div className="flex justify-between items-center py-3 px-4 border-b border-border">
131
  <h2 className="text-sm font-semibold text-light-text m-0">Conversations</h2>
132
  <button
src/index.css CHANGED
@@ -20,6 +20,10 @@
20
  --assistant-message-color: #1a1a1a;
21
  --hover-color: #f5f5f5;
22
  --active-color: #e6f2ff;
 
 
 
 
23
  }
24
 
25
  * {
@@ -549,3 +553,89 @@ body {
549
  @apply bg-gray-400;
550
  }
551
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  --assistant-message-color: #1a1a1a;
21
  --hover-color: #f5f5f5;
22
  --active-color: #e6f2ff;
23
+
24
+ /* Responsive sidebar width */
25
+ --mobile-sidebar-width: 100%;
26
+ --tablet-sidebar-width: 250px;
27
  }
28
 
29
  * {
 
553
  @apply bg-gray-400;
554
  }
555
  }
556
+
557
+ /* Responsive styles */
558
+ @media (max-width: 768px) {
559
+ .app-header {
560
+ padding: 0 1rem;
561
+ }
562
+
563
+ .app-title {
564
+ font-size: 1rem;
565
+ }
566
+
567
+ .sidebar {
568
+ position: fixed;
569
+ z-index: 50;
570
+ height: 100%;
571
+ width: 100%;
572
+ left: -100%;
573
+ transition: all 0.3s ease-in-out;
574
+ }
575
+
576
+ .sidebar.mobile-open {
577
+ left: 0;
578
+ }
579
+
580
+ /* 确保在移动设备上收起的侧边栏仍然可见 */
581
+ .sidebar.collapsed {
582
+ width: 50px;
583
+ left: 0;
584
+ }
585
+
586
+ .message-content {
587
+ max-width: 100%;
588
+ }
589
+
590
+ .settings-modal {
591
+ width: 95%;
592
+ max-height: 80vh;
593
+ }
594
+
595
+ .profiles-list {
596
+ flex-direction: column;
597
+ gap: 0.5rem;
598
+ }
599
+
600
+ .chat-input {
601
+ padding: 0.75rem;
602
+ }
603
+
604
+ .message-input {
605
+ font-size: 0.875rem;
606
+ }
607
+
608
+ .send-button {
609
+ padding: 0 1rem;
610
+ }
611
+ }
612
+
613
+ /* Small mobile devices */
614
+ @media (max-width: 480px) {
615
+ .app-header {
616
+ height: 50px;
617
+ }
618
+
619
+ .right-section {
620
+ gap: 0.5rem;
621
+ }
622
+
623
+ .profile-dropdown-button,
624
+ .settings-button {
625
+ padding: 0.25rem 0.5rem;
626
+ font-size: 0.75rem;
627
+ }
628
+
629
+ .chat-messages {
630
+ padding: 0.75rem;
631
+ }
632
+
633
+ .message-content {
634
+ padding: 0.75rem;
635
+ font-size: 0.875rem;
636
+ }
637
+
638
+ .welcome-screen h2 {
639
+ font-size: 1.25rem;
640
+ }
641
+ }
src/main.jsx CHANGED
@@ -3,9 +3,13 @@ import ReactDOM from 'react-dom/client'
3
  import App from './App'
4
  import './tailwind.css'
5
  import './index.css'
 
6
 
7
  ReactDOM.createRoot(document.getElementById('root')).render(
8
  <React.StrictMode>
9
  <App />
10
  </React.StrictMode>,
11
- )
 
 
 
 
3
  import App from './App'
4
  import './tailwind.css'
5
  import './index.css'
6
+ import { register as registerServiceWorker } from './serviceWorkerRegistration'
7
 
8
  ReactDOM.createRoot(document.getElementById('root')).render(
9
  <React.StrictMode>
10
  <App />
11
  </React.StrictMode>,
12
+ )
13
+
14
+ // Register service worker for PWA functionality
15
+ registerServiceWorker()
src/serviceWorkerRegistration.js ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This optional code is used to register a service worker.
2
+ // register() is not called by default.
3
+
4
+ // This lets the app load faster on subsequent visits in production, and gives
5
+ // it offline capabilities. However, it also means that developers (and users)
6
+ // will only see deployed updates on subsequent visits to a page, after all the
7
+ // existing tabs open on the page have been closed, since previously cached
8
+ // resources are updated in the background.
9
+
10
+ const isLocalhost = Boolean(
11
+ window.location.hostname === 'localhost' ||
12
+ // [::1] is the IPv6 localhost address.
13
+ window.location.hostname === '[::1]' ||
14
+ // 127.0.0.0/8 are considered localhost for IPv4.
15
+ window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
16
+ );
17
+
18
+ export function register(config) {
19
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
20
+ // The URL constructor is available in all browsers that support SW.
21
+ const publicUrl = new URL(import.meta.env.BASE_URL, window.location.href);
22
+ if (publicUrl.origin !== window.location.origin) {
23
+ // Our service worker won't work if BASE_URL is on a different origin
24
+ // from what our page is served on. This might happen if a CDN is used to
25
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
26
+ return;
27
+ }
28
+
29
+ window.addEventListener('load', () => {
30
+ const swUrl = `${import.meta.env.BASE_URL}sw.js`;
31
+
32
+ if (isLocalhost) {
33
+ // This is running on localhost. Let's check if a service worker still exists or not.
34
+ checkValidServiceWorker(swUrl, config);
35
+
36
+ // Add some additional logging to localhost, pointing developers to the
37
+ // service worker/PWA documentation.
38
+ navigator.serviceWorker.ready.then(() => {
39
+ console.log(
40
+ 'This web app is being served cache-first by a service ' +
41
+ 'worker. To learn more, visit https://cra.link/PWA'
42
+ );
43
+ });
44
+ } else {
45
+ // Is not localhost. Just register service worker
46
+ registerValidSW(swUrl, config);
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ function registerValidSW(swUrl, config) {
53
+ navigator.serviceWorker
54
+ .register(swUrl)
55
+ .then((registration) => {
56
+ registration.onupdatefound = () => {
57
+ const installingWorker = registration.installing;
58
+ if (installingWorker == null) {
59
+ return;
60
+ }
61
+ installingWorker.onstatechange = () => {
62
+ if (installingWorker.state === 'installed') {
63
+ if (navigator.serviceWorker.controller) {
64
+ // At this point, the updated precached content has been fetched,
65
+ // but the previous service worker will still serve the older
66
+ // content until all client tabs are closed.
67
+ console.log(
68
+ 'New content is available and will be used when all ' +
69
+ 'tabs for this page are closed. See https://cra.link/PWA.'
70
+ );
71
+
72
+ // Execute callback
73
+ if (config && config.onUpdate) {
74
+ config.onUpdate(registration);
75
+ }
76
+ } else {
77
+ // At this point, everything has been precached.
78
+ // It's the perfect time to display a
79
+ // "Content is cached for offline use." message.
80
+ console.log('Content is cached for offline use.');
81
+
82
+ // Execute callback
83
+ if (config && config.onSuccess) {
84
+ config.onSuccess(registration);
85
+ }
86
+ }
87
+ }
88
+ };
89
+ };
90
+ })
91
+ .catch((error) => {
92
+ console.error('Error during service worker registration:', error);
93
+ });
94
+ }
95
+
96
+ function checkValidServiceWorker(swUrl, config) {
97
+ // Check if the service worker can be found. If it can't reload the page.
98
+ fetch(swUrl, {
99
+ headers: { 'Service-Worker': 'script' },
100
+ })
101
+ .then((response) => {
102
+ // Ensure service worker exists, and that we really are getting a JS file.
103
+ const contentType = response.headers.get('content-type');
104
+ if (
105
+ response.status === 404 ||
106
+ (contentType != null && contentType.indexOf('javascript') === -1)
107
+ ) {
108
+ // No service worker found. Probably a different app. Reload the page.
109
+ navigator.serviceWorker.ready.then((registration) => {
110
+ registration.unregister().then(() => {
111
+ window.location.reload();
112
+ });
113
+ });
114
+ } else {
115
+ // Service worker found. Proceed as normal.
116
+ registerValidSW(swUrl, config);
117
+ }
118
+ })
119
+ .catch(() => {
120
+ console.log('No internet connection found. App is running in offline mode.');
121
+ });
122
+ }
123
+
124
+ export function unregister() {
125
+ if ('serviceWorker' in navigator) {
126
+ navigator.serviceWorker.ready
127
+ .then((registration) => {
128
+ registration.unregister();
129
+ })
130
+ .catch((error) => {
131
+ console.error(error.message);
132
+ });
133
+ }
134
+ }