Skip to content

Commit c7cc5f7

Browse files
committed
update client: add loader
1 parent b4a8d79 commit c7cc5f7

File tree

2 files changed

+180
-25
lines changed

2 files changed

+180
-25
lines changed
Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,59 @@
1-
const PulseLoader = () => (
2-
<div className="flex space-x-2">
3-
<div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse"></div>
4-
<div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse [animation-delay:0.2s]"></div>
5-
<div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse [animation-delay:0.4s]"></div>
6-
</div>
7-
);
1+
import React, { useEffect, useState } from 'react';
2+
3+
const PulseLoader = () => {
4+
const [dots, setDots] = useState('');
5+
6+
useEffect(() => {
7+
const interval = setInterval(() => {
8+
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
9+
}, 500);
10+
11+
return () => clearInterval(interval);
12+
}, []);
13+
14+
return (
15+
<div className="flex flex-col items-center space-y-4 p-4">
16+
<div className="flex items-center space-x-3">
17+
<div className="relative w-10 h-10">
18+
{/* Circular pulse animation */}
19+
<div className="absolute inset-0 bg-blue-500/20 rounded-full animate-ping" />
20+
<div className="relative w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
21+
<svg
22+
className="w-6 h-6 text-white"
23+
fill="none"
24+
stroke="currentColor"
25+
viewBox="0 0 24 24"
26+
>
27+
<path
28+
strokeLinecap="round"
29+
strokeLinejoin="round"
30+
strokeWidth={2}
31+
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
32+
/>
33+
</svg>
34+
</div>
35+
</div>
36+
<div className="flex flex-col">
37+
<span className="text-gray-700 font-medium">Atomasage</span>
38+
<div className="flex items-center space-x-1">
39+
<div className="w-2 h-2 bg-green-500 rounded-full" />
40+
<span className="text-gray-500 text-sm">thinking{dots}</span>
41+
</div>
42+
</div>
43+
</div>
44+
45+
{/* Typing indicator */}
46+
<div className="flex space-x-1">
47+
{[0, 1, 2].map((i) => (
48+
<div
49+
key={i}
50+
className={`w-2 h-2 bg-blue-500 rounded-full animate-bounce`}
51+
style={{ animationDelay: `${i * 200}ms` }}
52+
/>
53+
))}
54+
</div>
55+
</div>
56+
);
57+
};
858

959
export default PulseLoader;

apps/client/app/utils/JSONFormatter.ts

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,30 @@ class JSONFormatter {
55
* @returns A string containing the formatted HTML.
66
*/
77
public static format(json: any): string {
8-
return this.formatJSON(json, 0);
8+
return `
9+
<div class="json-formatter" style="
10+
max-width: 100%;
11+
overflow-wrap: break-word;
12+
word-wrap: break-word;
13+
word-break: break-word;
14+
">
15+
${this.formatJSON(json, 0)}
16+
</div>
17+
`;
18+
}
19+
20+
/**
21+
* Transforms camelCase, snake_case, and PascalCase text to space-separated words
22+
* @param text - The text to transform
23+
* @returns The transformed text
24+
*/
25+
private static transformKey(text: string): string {
26+
return text
27+
.replace(/([A-Z])/g, ' $1')
28+
.replace(/_/g, ' ')
29+
.trim()
30+
.toLowerCase()
31+
.replace(/^\w/, (c) => c.toUpperCase());
932
}
1033

1134
/**
@@ -31,20 +54,42 @@ class JSONFormatter {
3154
* @returns A string containing the formatted HTML.
3255
*/
3356
private static formatArray(array: any[], indentLevel: number): string {
34-
let html = '';
57+
if (array.length === 0) {
58+
return '<div style="color: #666;">(empty array)</div>';
59+
}
60+
61+
let html = '<div class="json-array" style="width: 100%;">';
3562
array.forEach((item, index) => {
3663
if (typeof item === 'object' && item !== null) {
37-
// Add a styled section for arrays of objects
38-
html += `<div style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #f9f9f9;">
39-
${this.formatJSON(item, indentLevel + 1)}
40-
</div>`;
64+
html += `
65+
<div class="json-object-container" style="
66+
margin: 12px 0;
67+
padding: 16px;
68+
border: 1px solid #ddd;
69+
border-radius: 8px;
70+
background: #f9f9f9;
71+
overflow-wrap: break-word;
72+
word-wrap: break-word;
73+
word-break: break-word;
74+
">
75+
${this.formatJSON(item, indentLevel + 1)}
76+
</div>
77+
`;
4178
} else {
42-
// Regular array items (non-objects)
43-
html += `<div style="margin-left: ${
44-
indentLevel * 20
45-
}px;">- ${this.formatJSON(item, indentLevel + 1)}</div>`;
79+
html += `
80+
<div class="json-array-item" style="
81+
padding: 4px 0;
82+
margin-left: ${indentLevel * 24}px;
83+
display: flex;
84+
align-items: flex-start;
85+
">
86+
<span style="margin-right: 8px;">•</span>
87+
<div style="flex: 1;">${this.formatJSON(item, indentLevel + 1)}</div>
88+
</div>
89+
`;
4690
}
4791
});
92+
html += '</div>';
4893
return html;
4994
}
5095

@@ -55,13 +100,64 @@ class JSONFormatter {
55100
* @returns A string containing the formatted HTML.
56101
*/
57102
private static formatObject(obj: { [key: string]: any }, indentLevel: number): string {
58-
let html = '';
103+
if (Object.keys(obj).length === 0) {
104+
return '<div style="color: #666;">(empty object)</div>';
105+
}
106+
107+
let html = '<div class="json-object" style="width: 100%;">';
59108
const keys = Object.keys(obj);
60109
keys.forEach((key, index) => {
61-
html += `<div style="margin-left: ${indentLevel * 20}px;">
62-
<strong>${key}:</strong> ${this.formatJSON(obj[key], indentLevel + 1)}
63-
</div>`;
110+
const value = obj[key];
111+
const isNested = typeof value === 'object' && value !== null;
112+
113+
if (isNested) {
114+
// Nested objects/arrays get their own block
115+
html += `
116+
<div class="json-property nested" style="
117+
padding: 8px 0;
118+
margin-left: ${indentLevel * 24}px;
119+
">
120+
<div style="
121+
font-weight: bold;
122+
margin-bottom: 8px;
123+
">${this.transformKey(key)}:</div>
124+
<div style="
125+
padding-left: 24px;
126+
">
127+
${this.formatJSON(value, indentLevel + 1)}
128+
</div>
129+
</div>
130+
`;
131+
} else {
132+
// Primitive values stay inline
133+
html += `
134+
<div class="json-property" style="
135+
padding: 8px 0;
136+
margin-left: ${indentLevel * 24}px;
137+
display: flex;
138+
flex-wrap: wrap;
139+
align-items: flex-start;
140+
">
141+
<strong style="
142+
min-width: 120px;
143+
max-width: 100%;
144+
margin-right: 12px;
145+
margin-bottom: 4px;
146+
">${this.transformKey(key)}:</strong>
147+
<div style="
148+
flex: 1;
149+
min-width: 200px;
150+
overflow-wrap: break-word;
151+
word-wrap: break-word;
152+
word-break: break-word;
153+
">
154+
${this.formatJSON(value, indentLevel + 1)}
155+
</div>
156+
</div>
157+
`;
158+
}
64159
});
160+
html += '</div>';
65161
return html;
66162
}
67163

@@ -72,15 +168,24 @@ class JSONFormatter {
72168
* @returns A string containing the formatted HTML.
73169
*/
74170
private static formatPrimitive(value: any, indentLevel: number): string {
171+
const style = `
172+
color: #444;
173+
font-family: monospace;
174+
overflow-wrap: break-word;
175+
word-wrap: break-word;
176+
word-break: break-word;
177+
`;
178+
75179
if (typeof value === 'string') {
76-
return `<span>"${value}"</span>`; // Wrap strings in quotes for clarity
180+
return `<span style="${style}">"${value}"</span>`;
77181
} else if (value === null) {
78-
return '<span>null</span>'; // Handle null values
182+
return `<span style="${style}">null</span>`;
79183
} else if (typeof value === 'undefined') {
80-
return '<span>undefined</span>'; // Handle undefined values
184+
return `<span style="${style}">undefined</span>`;
81185
} else {
82-
return `<span>${value.toString()}</span>`; // Numbers, booleans, etc.
186+
return `<span style="${style}">${value.toString()}</span>`;
83187
}
84188
}
85189
}
190+
86191
export default JSONFormatter;

0 commit comments

Comments
 (0)