app-tutor-ai-consumer 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/staging-staging.yml +148 -0
- package/.github/workflows/staging.yml +1 -2
- package/CHANGELOG.md +25 -0
- package/config/rspack/rspack.config.js +5 -1
- package/config/vitest/__mocks__/icons.tsx +3 -0
- package/config/vitest/__mocks__/intersection-observer.ts +10 -0
- package/config/vitest/__mocks__/sparkie.tsx +2 -11
- package/config/vitest/__mocks__/use-init-sparkie.tsx +14 -0
- package/config/vitest/setupTests.ts +6 -1
- package/config/vitest/vitest.config.mts +13 -8
- package/environments/.env.test +2 -0
- package/package.json +13 -4
- package/public/assets/images/default-image.png +0 -0
- package/public/index.html +3 -4
- package/src/@types/declarations.d.ts +12 -3
- package/src/config/dayjs/index.ts +2 -0
- package/src/config/dayjs/init.ts +28 -0
- package/src/config/dayjs/utils/format-fulldate.ts +7 -0
- package/src/config/dayjs/utils/format-time.ts +20 -0
- package/src/config/dayjs/utils/index.ts +2 -0
- package/src/config/styles/global.css +21 -3
- package/src/config/tanstack/query-client.ts +2 -1
- package/src/config/tanstack/query-provider.tsx +1 -1
- package/src/config/tests/handlers.ts +6 -0
- package/src/config/tests/utils.tsx +3 -2
- package/src/config/tests/wrappers.tsx +4 -1
- package/src/index.tsx +26 -0
- package/src/lib/components/icons/arrow-down.svg +5 -0
- package/src/lib/components/icons/chevron-down.svg +4 -0
- package/src/lib/components/icons/icon-names.d.ts +1 -1
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/__tests__/markdown.stub.ts +334 -0
- package/src/lib/components/markdownrenderer/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/components/md-code-block/index.ts +1 -0
- package/src/lib/components/markdownrenderer/components/md-code-block/md-code-block.tsx +71 -0
- package/src/lib/components/markdownrenderer/index.ts +2 -0
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +113 -0
- package/src/lib/hooks/index.ts +3 -0
- package/src/lib/hooks/use-intersection-observer-reverse-scroll/index.ts +2 -0
- package/src/lib/hooks/use-intersection-observer-reverse-scroll/use-intersection-observer-reverse-scroll.tsx +147 -0
- package/src/lib/hooks/use-ref-client-height/index.ts +2 -0
- package/src/lib/hooks/use-ref-client-height/use-ref-client-height.tsx +38 -0
- package/src/lib/hooks/use-scroll-to-ref/index.ts +2 -0
- package/src/lib/hooks/use-scroll-to-ref/use-scroll-to-ref.tsx +14 -0
- package/src/lib/hooks/use-throttle/index.ts +3 -0
- package/src/lib/hooks/use-throttle/types.ts +13 -0
- package/src/lib/hooks/use-throttle/use-throttle.spec.tsx +296 -0
- package/src/lib/hooks/use-throttle/use-throttle.tsx +91 -0
- package/src/lib/utils/constants.ts +1 -1
- package/src/lib/utils/copy-text-to-clipboard.tsx +13 -0
- package/src/lib/utils/extract-text-from-react-nodes.ts +23 -0
- package/src/lib/utils/index.ts +3 -0
- package/src/lib/utils/urls.ts +20 -0
- package/src/main/main.spec.tsx +9 -0
- package/src/modules/cursor/__tests__/icursor-update.builder.ts +42 -0
- package/src/modules/cursor/hooks/index.ts +1 -0
- package/src/modules/cursor/hooks/use-update-cursor/index.ts +2 -0
- package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.spec.tsx +23 -0
- package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.ts +11 -0
- package/src/modules/cursor/index.ts +2 -0
- package/src/modules/cursor/service.ts +15 -0
- package/src/modules/cursor/types.ts +9 -0
- package/src/modules/global-providers/index.ts +1 -0
- package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +113 -0
- package/src/modules/messages/__tests__/imessage-with-sender-data.mock.ts +15 -0
- package/src/modules/messages/__tests__/parsed-message.builder.ts +164 -0
- package/src/modules/messages/components/index.ts +2 -0
- package/src/modules/messages/components/message-img/index.ts +1 -0
- package/src/modules/messages/components/message-img/message-img.tsx +47 -0
- package/src/modules/messages/components/message-item/index.ts +2 -0
- package/src/modules/messages/components/message-item/message-item.spec.tsx +26 -0
- package/src/modules/messages/components/message-item/message-item.tsx +28 -0
- package/src/modules/messages/components/message-item-end-of-scroll/index.ts +2 -0
- package/src/modules/messages/components/message-item-end-of-scroll/message-item-end-of-scroll.tsx +14 -0
- package/src/modules/messages/components/message-item-error/index.ts +2 -0
- package/src/modules/messages/components/message-item-error/message-item-error.tsx +25 -0
- package/src/modules/messages/components/message-item-loading/index.ts +2 -0
- package/src/modules/messages/components/message-item-loading/message-item-loading.tsx +16 -0
- package/src/modules/messages/components/messages-list/index.ts +1 -1
- package/src/modules/messages/components/messages-list/messages-list.tsx +76 -17
- package/src/modules/messages/constants.ts +1 -0
- package/src/modules/messages/hooks/index.ts +4 -0
- package/src/modules/messages/hooks/use-all-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +30 -0
- package/src/modules/messages/hooks/use-fetch-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.spec.tsx +46 -0
- package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.tsx +103 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +58 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +97 -0
- package/src/modules/messages/hooks/use-manage-scroll/index.ts +2 -0
- package/src/modules/messages/hooks/use-manage-scroll/use-manage-scroll.tsx +66 -0
- package/src/modules/messages/service.ts +26 -3
- package/src/modules/messages/types.ts +33 -2
- package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.spec.tsx +58 -0
- package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.ts +30 -0
- package/src/modules/messages/utils/has-to-update-cursor/index.ts +2 -0
- package/src/modules/messages/utils/index.ts +1 -0
- package/src/modules/messages/utils/messages-parser/index.ts +1 -0
- package/src/modules/messages/utils/messages-parser/utils.ts +28 -0
- package/src/modules/profile/__tests__/profile-api-props.builder.ts +74 -0
- package/src/modules/profile/__tests__/profile-props.builder.ts +42 -0
- package/src/modules/profile/constants.ts +3 -0
- package/src/modules/profile/hooks/index.ts +1 -0
- package/src/modules/profile/hooks/use-get-profile/index.ts +1 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.spec.tsx +20 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +14 -0
- package/src/modules/profile/index.ts +4 -0
- package/src/modules/profile/service.tsx +19 -0
- package/src/modules/profile/types.ts +17 -0
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +33 -0
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +6 -0
- package/src/modules/widget/components/chat-page/chat-page.spec.tsx +28 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +3 -5
- package/src/modules/widget/components/container/container.tsx +21 -15
- package/src/modules/widget/components/index.ts +1 -0
- package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +16 -14
- package/src/modules/widget/components/scroll-to-bottom-button/index.ts +2 -0
- package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +32 -0
- package/src/modules/widget/components/starter-page/starter-page.tsx +3 -3
- package/src/modules/widget/events.ts +4 -0
- package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +8 -6
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-container-intrinsic-height.atom.ts +13 -0
- package/src/modules/widget/store/widget-settings.atom.ts +3 -1
- package/src/config/styles/shared-styles.module.css +0 -16
- package/src/modules/widget/components/container/styles.module.css +0 -11
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated file - DO NOT EDIT
|
|
2
|
-
export type ValidIconNames = 'send'
|
|
2
|
+
export type ValidIconNames = 'arrow-down' | 'chevron-down' | 'send'
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
export const TEST_MARKDOWN_STUB = `# Main Heading - Testing Markdown Renderer
|
|
2
|
+
|
|
3
|
+
This is a comprehensive test for our markdown renderer component. Let's see how it handles various elements.
|
|
4
|
+
|
|
5
|
+
## Subheading Level 2
|
|
6
|
+
|
|
7
|
+
### Subheading Level 3
|
|
8
|
+
|
|
9
|
+
#### Subheading Level 4
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Text Formatting
|
|
14
|
+
|
|
15
|
+
Here's some **bold text** and *italic text*. You can also combine them for ***bold and italic***.
|
|
16
|
+
|
|
17
|
+
We also support ~~strikethrough text~~ and \`inline code snippets\`.
|
|
18
|
+
|
|
19
|
+
> This is a blockquote. It should stand out from regular text.
|
|
20
|
+
>
|
|
21
|
+
> Blockquotes can span multiple lines and even contain other markdown elements like **bold text**.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Links
|
|
26
|
+
|
|
27
|
+
- [External Link to React Documentation](https://reactjs.org/docs/getting-started.html)
|
|
28
|
+
- [Internal Link](#code-examples)
|
|
29
|
+
- [Email Link](mailto:test@example.com)
|
|
30
|
+
- Auto-link: https://github.com
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Images
|
|
35
|
+
|
|
36
|
+
### Regular Image
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
### Image with Alt Text
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
### Broken Image (for error handling)
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Lists
|
|
48
|
+
|
|
49
|
+
### Unordered List
|
|
50
|
+
- First item
|
|
51
|
+
- Second item with **bold text**
|
|
52
|
+
- Third item with [a link](https://example.com)
|
|
53
|
+
- Nested item 1
|
|
54
|
+
- Nested item 2
|
|
55
|
+
- Double nested item
|
|
56
|
+
- Fourth item with \`inline code\`
|
|
57
|
+
|
|
58
|
+
### Ordered List
|
|
59
|
+
1. First numbered item
|
|
60
|
+
2. Second numbered item
|
|
61
|
+
3. Third item with multiple lines
|
|
62
|
+
|
|
63
|
+
This paragraph is part of item 3.
|
|
64
|
+
|
|
65
|
+
4. Fourth item
|
|
66
|
+
1. Nested numbered item
|
|
67
|
+
2. Another nested item
|
|
68
|
+
|
|
69
|
+
### Task List (GitHub Flavored Markdown)
|
|
70
|
+
- [x] Completed task
|
|
71
|
+
- [ ] Incomplete task
|
|
72
|
+
- [x] Another completed task
|
|
73
|
+
- [ ] Task with **bold text**
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Code Examples
|
|
78
|
+
|
|
79
|
+
### Inline Code
|
|
80
|
+
Use the \`useState\` hook for state management in React.
|
|
81
|
+
|
|
82
|
+
### JavaScript Code Block
|
|
83
|
+
\`\`\`javascript
|
|
84
|
+
function greetUser(name) {
|
|
85
|
+
if (!name) {
|
|
86
|
+
return "Hello, World!";
|
|
87
|
+
}
|
|
88
|
+
return \`Hello, \${name}!\`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const user = "John Doe";
|
|
92
|
+
console.log(greetUser(user));
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
### TypeScript React Component
|
|
96
|
+
\`\`\`tsx
|
|
97
|
+
import React, { useState, useEffect } from 'react';
|
|
98
|
+
|
|
99
|
+
interface UserProps {
|
|
100
|
+
id: number;
|
|
101
|
+
name: string;
|
|
102
|
+
email?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const UserComponent: React.FC<UserProps> = ({ id, name, email }) => {
|
|
106
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
console.log(\`User \${name} loaded\`);
|
|
110
|
+
}, [name]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="user-card">
|
|
114
|
+
<h2>{name}</h2>
|
|
115
|
+
{email && <p>Email: {email}</p>}
|
|
116
|
+
<button onClick={() => setIsLoading(!isLoading)}>
|
|
117
|
+
{isLoading ? 'Loading...' : 'Load Data'}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default UserComponent;
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
### CSS Code Block
|
|
127
|
+
\`\`\`css
|
|
128
|
+
.markdown-container {
|
|
129
|
+
max-width: 800px;
|
|
130
|
+
margin: 0 auto;
|
|
131
|
+
padding: 2rem;
|
|
132
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.code-block {
|
|
136
|
+
background-color: #f6f8fa;
|
|
137
|
+
border-radius: 6px;
|
|
138
|
+
padding: 16px;
|
|
139
|
+
overflow-x: auto;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (max-width: 768px) {
|
|
143
|
+
.markdown-container {
|
|
144
|
+
padding: 1rem;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
### JSON Data
|
|
150
|
+
\`\`\`json
|
|
151
|
+
{
|
|
152
|
+
"name": "my-react-app",
|
|
153
|
+
"version": "1.0.0",
|
|
154
|
+
"dependencies": {
|
|
155
|
+
"react": "^18.2.0",
|
|
156
|
+
"react-dom": "^18.2.0",
|
|
157
|
+
"react-markdown": "^8.0.7"
|
|
158
|
+
},
|
|
159
|
+
"scripts": {
|
|
160
|
+
"dev": "vite",
|
|
161
|
+
"build": "vite build",
|
|
162
|
+
"test": "vitest"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
### Bash Commands
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
# Install dependencies
|
|
170
|
+
npm install react-markdown
|
|
171
|
+
|
|
172
|
+
# Start development server
|
|
173
|
+
npm run dev
|
|
174
|
+
|
|
175
|
+
# Build for production
|
|
176
|
+
npm run build
|
|
177
|
+
|
|
178
|
+
# Run tests
|
|
179
|
+
npm test
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
### Python Code
|
|
183
|
+
\`\`\`python
|
|
184
|
+
def fibonacci(n):
|
|
185
|
+
"""Generate Fibonacci sequence up to n terms."""
|
|
186
|
+
if n <= 0:
|
|
187
|
+
return []
|
|
188
|
+
elif n == 1:
|
|
189
|
+
return [0]
|
|
190
|
+
elif n == 2:
|
|
191
|
+
return [0, 1]
|
|
192
|
+
|
|
193
|
+
sequence = [0, 1]
|
|
194
|
+
for i in range(2, n):
|
|
195
|
+
sequence.append(sequence[i-1] + sequence[i-2])
|
|
196
|
+
|
|
197
|
+
return sequence
|
|
198
|
+
|
|
199
|
+
# Example usage
|
|
200
|
+
print(fibonacci(10))
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Tables
|
|
206
|
+
|
|
207
|
+
### Simple Table
|
|
208
|
+
| Name | Age | City |
|
|
209
|
+
|------|-----|------|
|
|
210
|
+
| John | 25 | New York |
|
|
211
|
+
| Jane | 30 | Los Angeles |
|
|
212
|
+
| Bob | 35 | Chicago |
|
|
213
|
+
|
|
214
|
+
### Complex Table with Alignment
|
|
215
|
+
| Feature | React | Vue | Angular |
|
|
216
|
+
|:--------|:-----:|:---:|--------:|
|
|
217
|
+
| Learning Curve | Easy | Easy | Steep |
|
|
218
|
+
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
|
219
|
+
| Community | Large | Medium | Large |
|
|
220
|
+
| Bundle Size | Small | Small | Large |
|
|
221
|
+
|
|
222
|
+
### Table with Code and Links
|
|
223
|
+
| Language | Extension | Documentation |
|
|
224
|
+
|----------|-----------|---------------|
|
|
225
|
+
| JavaScript | \`.js\` | [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript) |
|
|
226
|
+
| TypeScript | \`.ts\` | [TS Docs](https://www.typescriptlang.org/docs/) |
|
|
227
|
+
| Python | \`.py\` | [Python Docs](https://docs.python.org/3/) |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Special Characters and Escaping
|
|
232
|
+
|
|
233
|
+
Here are some special characters: & < > " '
|
|
234
|
+
|
|
235
|
+
HTML entities: & < > " '
|
|
236
|
+
|
|
237
|
+
Markdown escaping: \\* \\_ \\# \\[ \\] \\( \\)
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Horizontal Rules
|
|
242
|
+
|
|
243
|
+
Above this line is a horizontal rule.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
Below this line is another horizontal rule.
|
|
248
|
+
|
|
249
|
+
***
|
|
250
|
+
|
|
251
|
+
And here's one more made with asterisks.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Mathematical Expressions (if supported)
|
|
256
|
+
|
|
257
|
+
Inline math: $E = mc^2$
|
|
258
|
+
|
|
259
|
+
Block math:
|
|
260
|
+
$$
|
|
261
|
+
\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
|
|
262
|
+
$$
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## HTML Elements (if raw HTML is enabled)
|
|
267
|
+
|
|
268
|
+
<div style="background-color: #f0f0f0; padding: 10px; border-radius: 5px;">
|
|
269
|
+
This is raw HTML content within markdown.
|
|
270
|
+
<br>
|
|
271
|
+
<strong>Bold text via HTML</strong>
|
|
272
|
+
<br>
|
|
273
|
+
<em>Italic text via HTML</em>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<details>
|
|
277
|
+
<summary>Click to expand</summary>
|
|
278
|
+
|
|
279
|
+
This content is hidden by default and can be toggled.
|
|
280
|
+
|
|
281
|
+
- Item 1
|
|
282
|
+
- Item 2
|
|
283
|
+
- Item 3
|
|
284
|
+
|
|
285
|
+
</details>
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Edge Cases
|
|
290
|
+
|
|
291
|
+
### Empty Code Block
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
### Code Block Without Language
|
|
297
|
+
\`\`\`
|
|
298
|
+
function noLanguageSpecified() {
|
|
299
|
+
return "This should still render";
|
|
300
|
+
}
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
### Very Long Line
|
|
304
|
+
This is a very long line that should test how the renderer handles text wrapping and overflow in different containers and screen sizes to ensure proper responsive behavior.
|
|
305
|
+
|
|
306
|
+
### Unicode and Emojis
|
|
307
|
+
Unicode characters: café, naïve, résumé
|
|
308
|
+
Emojis: 🚀 ⚡ 💻 🎉 ✅ ❌ 🔥 💡
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Conclusion
|
|
313
|
+
|
|
314
|
+
This markdown document tests:
|
|
315
|
+
|
|
316
|
+
✅ **Headings** (H1-H6)
|
|
317
|
+
✅ **Text formatting** (bold, italic, strikethrough)
|
|
318
|
+
✅ **Links** (external, internal, email, auto-links)
|
|
319
|
+
✅ **Images** (valid and invalid URLs)
|
|
320
|
+
✅ **Lists** (ordered, unordered, nested, task lists)
|
|
321
|
+
✅ **Code blocks** (multiple languages, inline code)
|
|
322
|
+
✅ **Tables** (simple and complex)
|
|
323
|
+
✅ **Blockquotes**
|
|
324
|
+
✅ **Horizontal rules**
|
|
325
|
+
✅ **Special characters and escaping**
|
|
326
|
+
✅ **HTML elements** (if enabled)
|
|
327
|
+
✅ **Edge cases**
|
|
328
|
+
|
|
329
|
+
If all these elements render correctly, your markdown component is working properly! 🎉
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
*Last updated: ${new Date().toLocaleDateString()}*
|
|
334
|
+
`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './md-code-block'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './md-code-block'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { memo, useState } from 'react'
|
|
2
|
+
import { Highlight, themes } from 'prism-react-renderer'
|
|
3
|
+
import type { PropsWithChildren } from 'react'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
|
|
6
|
+
import { copyTextToClipboard, extractTextFromReactNodes } from '@/src/lib/utils'
|
|
7
|
+
|
|
8
|
+
export type MdCodeBlockProps = PropsWithChildren<{
|
|
9
|
+
inline?: boolean
|
|
10
|
+
className?: string
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
const MdCodeBlock = memo(({ inline, className, children, ...props }: MdCodeBlockProps) => {
|
|
14
|
+
const { t } = useTranslation()
|
|
15
|
+
const [copied, setCopied] = useState(false)
|
|
16
|
+
const codeContent = extractTextFromReactNodes(children)
|
|
17
|
+
const match = /language-(\w+)/.exec(className || '')
|
|
18
|
+
const language = match ? match[1] : 'text'
|
|
19
|
+
|
|
20
|
+
const handleCopy = () => {
|
|
21
|
+
setCopied(true)
|
|
22
|
+
void copyTextToClipboard(codeContent)
|
|
23
|
+
.then(
|
|
24
|
+
(result) =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
if (!result) return reject(new Error('copy failed'))
|
|
27
|
+
setTimeout(resolve, 2000)
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
.finally(() => setCopied(false))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (inline) {
|
|
34
|
+
return (
|
|
35
|
+
<code className='rounded bg-neutral-800 px-2 py-1 font-mono text-sm text-pink-600' {...props}>
|
|
36
|
+
{children}
|
|
37
|
+
</code>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className='group relative'>
|
|
43
|
+
<div className='flex items-center justify-between rounded-t-lg bg-neutral-800 px-4 py-2 text-sm text-neutral-300'>
|
|
44
|
+
<span className='font-mono text-xs uppercase tracking-wide'>{language}</span>
|
|
45
|
+
<button
|
|
46
|
+
onClick={handleCopy}
|
|
47
|
+
className='rounded bg-neutral-700 px-2 py-1 text-xs opacity-0 transition-opacity duration-200 hover:bg-neutral-600 hover:text-neutral-0 group-hover:opacity-100'>
|
|
48
|
+
{t(copied ? `general.buttons.copied` : 'general.buttons.copy')}
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<Highlight theme={themes.vsDark} code={codeContent} language={language}>
|
|
53
|
+
{({ className: highlightClassName, style, tokens, getLineProps, getTokenProps }) => (
|
|
54
|
+
<pre className={`${highlightClassName} overflow-x-auto rounded-b-lg p-4`} style={style}>
|
|
55
|
+
{tokens.map((line, i) => (
|
|
56
|
+
<div key={i} {...getLineProps({ line })}>
|
|
57
|
+
{line.map((token, key) => (
|
|
58
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</pre>
|
|
63
|
+
)}
|
|
64
|
+
</Highlight>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
MdCodeBlock.displayName = 'MdCodeBlock'
|
|
70
|
+
|
|
71
|
+
export default MdCodeBlock
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import Markdown, { type Components } from 'react-markdown'
|
|
3
|
+
import rehypeRaw from 'rehype-raw'
|
|
4
|
+
import rehypeSanitize from 'rehype-sanitize'
|
|
5
|
+
import remarkBreaks from 'remark-breaks'
|
|
6
|
+
import remarkGfm from 'remark-gfm'
|
|
7
|
+
|
|
8
|
+
import { URLutils } from '../../utils'
|
|
9
|
+
|
|
10
|
+
import MdCodeBlock from './components/md-code-block'
|
|
11
|
+
|
|
12
|
+
const mdComponents: Partial<Components> = {
|
|
13
|
+
h1({ children, ...props }) {
|
|
14
|
+
return (
|
|
15
|
+
<h1 className='mb-4 border-b pb-2 text-2xl font-bold' {...props}>
|
|
16
|
+
{children}
|
|
17
|
+
</h1>
|
|
18
|
+
)
|
|
19
|
+
},
|
|
20
|
+
h2({ children, ...props }) {
|
|
21
|
+
return (
|
|
22
|
+
<h2 className='mb-3 mt-6 text-xl font-semibold' {...props}>
|
|
23
|
+
{children}
|
|
24
|
+
</h2>
|
|
25
|
+
)
|
|
26
|
+
},
|
|
27
|
+
code(props) {
|
|
28
|
+
return <MdCodeBlock {...props} />
|
|
29
|
+
},
|
|
30
|
+
pre({ children }) {
|
|
31
|
+
return <span className='my-2 inline-block w-full overflow-hidden rounded-lg'>{children}</span>
|
|
32
|
+
},
|
|
33
|
+
a({ href, children, ...props }) {
|
|
34
|
+
const url = URLutils.getURLwithProtocol(href)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<a
|
|
38
|
+
href={url}
|
|
39
|
+
target={url?.startsWith('http') ? '_blank' : '_self'}
|
|
40
|
+
rel={url?.startsWith('http') ? 'noopener noreferrer' : undefined}
|
|
41
|
+
className='inline-block break-all text-blue-600 underline hover:text-blue-800'
|
|
42
|
+
{...props}>
|
|
43
|
+
{children}
|
|
44
|
+
</a>
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
table({ children, ...props }) {
|
|
48
|
+
return (
|
|
49
|
+
<div className='overflow-x-auto'>
|
|
50
|
+
<table className='min-w-full border-collapse border border-neutral-800' {...props}>
|
|
51
|
+
{children}
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
th({ children, ...props }) {
|
|
57
|
+
return (
|
|
58
|
+
<th className='border border-neutral-800 px-4 py-2 text-left font-semibold' {...props}>
|
|
59
|
+
{children}
|
|
60
|
+
</th>
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
td({ children, ...props }) {
|
|
64
|
+
return (
|
|
65
|
+
<td className='border border-neutral-800 px-4 py-2' {...props}>
|
|
66
|
+
{children}
|
|
67
|
+
</td>
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
blockquote({ children, ...props }) {
|
|
71
|
+
return (
|
|
72
|
+
<blockquote
|
|
73
|
+
className='my-2 border-l-4 border-primary-500 pl-4 italic text-neutral-100'
|
|
74
|
+
{...props}>
|
|
75
|
+
{children}
|
|
76
|
+
</blockquote>
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
p({ children }) {
|
|
80
|
+
return <span className='my-3 inline-block'>{children}</span>
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type MarkdownRendererProps = {
|
|
85
|
+
content?: string
|
|
86
|
+
className?: string
|
|
87
|
+
allowDangerousHtml?: boolean
|
|
88
|
+
enableGfm?: boolean
|
|
89
|
+
imgComponent?: Components['img']
|
|
90
|
+
}
|
|
91
|
+
function MarkdownRenderer({
|
|
92
|
+
content,
|
|
93
|
+
allowDangerousHtml,
|
|
94
|
+
className,
|
|
95
|
+
enableGfm = true,
|
|
96
|
+
imgComponent
|
|
97
|
+
}: MarkdownRendererProps) {
|
|
98
|
+
const remarkPlugins = [...(enableGfm ? [remarkGfm] : []), remarkBreaks]
|
|
99
|
+
const rehypePlugins = [rehypeSanitize, ...(allowDangerousHtml ? [rehypeRaw] : [])]
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className={clsx('max-w-none', className)}>
|
|
103
|
+
<Markdown
|
|
104
|
+
remarkPlugins={remarkPlugins}
|
|
105
|
+
rehypePlugins={rehypePlugins}
|
|
106
|
+
components={!imgComponent ? mdComponents : { ...mdComponents, img: imgComponent }}>
|
|
107
|
+
{content}
|
|
108
|
+
</Markdown>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default MarkdownRenderer
|
package/src/lib/hooks/index.ts
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useThrottle } from '../use-throttle'
|
|
4
|
+
|
|
5
|
+
export type UseIntersectionObserverReverseScrollProps = {
|
|
6
|
+
hasMore: boolean
|
|
7
|
+
asyncCallback: () => Promise<void>
|
|
8
|
+
onScroll?: () => void
|
|
9
|
+
isLoading?: boolean
|
|
10
|
+
threshold?: number
|
|
11
|
+
rootMargin?: string
|
|
12
|
+
enabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function useIntersectionObserverReverseScroll({
|
|
16
|
+
hasMore,
|
|
17
|
+
asyncCallback,
|
|
18
|
+
onScroll,
|
|
19
|
+
enabled = true,
|
|
20
|
+
isLoading = false,
|
|
21
|
+
rootMargin = '100px 0px',
|
|
22
|
+
threshold = 0.5
|
|
23
|
+
}: UseIntersectionObserverReverseScrollProps) {
|
|
24
|
+
const [heightBeforeRender, setHeightBeforeRender] = useState(0)
|
|
25
|
+
const [shouldRestorePosition, setShouldRestorePosition] = useState(false)
|
|
26
|
+
const [hasUserScrolled, setHasUserScrolled] = useState(false)
|
|
27
|
+
const [isInitialLoad, setIsInitialLoad] = useState(true)
|
|
28
|
+
|
|
29
|
+
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
30
|
+
const intersectionTargetRef = useRef<HTMLDivElement>(null)
|
|
31
|
+
const observerRef = useRef<IntersectionObserver | null>(null)
|
|
32
|
+
|
|
33
|
+
const { throttledCallback: throttledHandleScroll } = useThrottle({
|
|
34
|
+
callback: () => {
|
|
35
|
+
onScroll?.()
|
|
36
|
+
},
|
|
37
|
+
delay: 50
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const handleScroll = useCallback(() => {
|
|
41
|
+
const { current: scroller } = scrollerRef
|
|
42
|
+
|
|
43
|
+
if (!scroller) return
|
|
44
|
+
|
|
45
|
+
if (!hasUserScrolled) setHasUserScrolled(true)
|
|
46
|
+
|
|
47
|
+
if (!isInitialLoad) setIsInitialLoad(true)
|
|
48
|
+
|
|
49
|
+
throttledHandleScroll()
|
|
50
|
+
}, [hasUserScrolled, isInitialLoad, throttledHandleScroll])
|
|
51
|
+
|
|
52
|
+
const handleIntersection = useCallback(
|
|
53
|
+
async (entries: IntersectionObserverEntry[]) => {
|
|
54
|
+
const [entry] = entries
|
|
55
|
+
const { current: scroller } = scrollerRef
|
|
56
|
+
|
|
57
|
+
if (!enabled || !scroller || !isInitialLoad || !hasUserScrolled || isLoading || !hasMore)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if (entry?.isIntersecting) {
|
|
61
|
+
setHeightBeforeRender(scroller.scrollHeight)
|
|
62
|
+
setShouldRestorePosition(true)
|
|
63
|
+
await asyncCallback()
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[enabled, hasMore, hasUserScrolled, isInitialLoad, isLoading, asyncCallback]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const scrollToBottom = useCallback((smooth = false) => {
|
|
70
|
+
const { current: scroller } = scrollerRef
|
|
71
|
+
|
|
72
|
+
if (!scroller) return
|
|
73
|
+
|
|
74
|
+
scroller.scrollTo({
|
|
75
|
+
top: scroller.scrollHeight,
|
|
76
|
+
behavior: smooth ? 'smooth' : 'auto'
|
|
77
|
+
})
|
|
78
|
+
}, [])
|
|
79
|
+
|
|
80
|
+
const resetScrollState = useCallback(() => {
|
|
81
|
+
setHasUserScrolled(false)
|
|
82
|
+
setIsInitialLoad(true)
|
|
83
|
+
setShouldRestorePosition(false)
|
|
84
|
+
setHeightBeforeRender(0)
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const { current: scroller } = scrollerRef
|
|
89
|
+
const { current: intersectionTarget } = intersectionTargetRef
|
|
90
|
+
|
|
91
|
+
if (!intersectionTarget || !scroller) return
|
|
92
|
+
|
|
93
|
+
observerRef.current = new IntersectionObserver((entries) => void handleIntersection(entries), {
|
|
94
|
+
root: scroller,
|
|
95
|
+
rootMargin,
|
|
96
|
+
threshold
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
observerRef.current.observe(intersectionTarget)
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
observerRef.current?.disconnect()
|
|
103
|
+
}
|
|
104
|
+
}, [handleIntersection, rootMargin, threshold])
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const { current: scroller } = scrollerRef
|
|
108
|
+
const controller = new AbortController()
|
|
109
|
+
|
|
110
|
+
if (!scroller) return
|
|
111
|
+
|
|
112
|
+
scroller.addEventListener('scroll', handleScroll, { passive: true, signal: controller.signal })
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
controller.abort()
|
|
116
|
+
}
|
|
117
|
+
}, [handleScroll])
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const { current: scroller } = scrollerRef
|
|
121
|
+
|
|
122
|
+
if (!shouldRestorePosition || isLoading || !scroller) return
|
|
123
|
+
|
|
124
|
+
const frameId = requestAnimationFrame(() => {
|
|
125
|
+
const newScrollTop = scroller.scrollHeight - heightBeforeRender
|
|
126
|
+
|
|
127
|
+
scroller.scrollTop = newScrollTop
|
|
128
|
+
setShouldRestorePosition(false)
|
|
129
|
+
setHeightBeforeRender(0)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
cancelAnimationFrame(frameId)
|
|
134
|
+
}
|
|
135
|
+
}, [heightBeforeRender, isLoading, shouldRestorePosition])
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
scrollerRef,
|
|
139
|
+
intersectionTargetRef,
|
|
140
|
+
scrollToBottom,
|
|
141
|
+
resetScrollState,
|
|
142
|
+
hasUserScrolled,
|
|
143
|
+
isInitialLoad
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default useIntersectionObserverReverseScroll
|