tlrd-extension 1.0.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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.claude/skills/ostovjs-expert/SKILL.md +72 -0
  3. package/README.md +74 -0
  4. package/icons/icon.svg +4 -0
  5. package/icons/icon128.png +0 -0
  6. package/icons/icon16.png +0 -0
  7. package/icons/icon48.png +0 -0
  8. package/manifest.json +26 -0
  9. package/package.json +25 -0
  10. package/postcss.config.js +7 -0
  11. package/src/app/app.html +13 -0
  12. package/src/app/app.ts +50 -0
  13. package/src/app/models/MessageModel.ts +17 -0
  14. package/src/app/models/MessagesCollection.ts +18 -0
  15. package/src/app/models/PageModel.ts +33 -0
  16. package/src/app/models/SettingsModel.ts +61 -0
  17. package/src/app/services/openai.ts +134 -0
  18. package/src/app/styles/chat.css +38 -0
  19. package/src/app/styles/input.css +13 -0
  20. package/src/app/styles/language-selector.css +18 -0
  21. package/src/app/styles/layout.css +49 -0
  22. package/src/app/styles/loader.css +31 -0
  23. package/src/app/styles/message.css +37 -0
  24. package/src/app/styles/scrollbar.css +17 -0
  25. package/src/app/styles/settings.css +55 -0
  26. package/src/app/styles/tldr.css +49 -0
  27. package/src/app/templates/app-header.hbs +28 -0
  28. package/src/app/templates/chat.hbs +22 -0
  29. package/src/app/templates/language-selector.hbs +16 -0
  30. package/src/app/templates/message.hbs +2 -0
  31. package/src/app/templates/settings.hbs +31 -0
  32. package/src/app/templates/tldr.hbs +29 -0
  33. package/src/app/views/AppView.ts +179 -0
  34. package/src/app/views/ChatView.ts +95 -0
  35. package/src/app/views/LanguageSelectorView.ts +52 -0
  36. package/src/app/views/MessageView.ts +33 -0
  37. package/src/app/views/SettingsView.ts +81 -0
  38. package/src/app/views/TldrView.ts +39 -0
  39. package/src/background/background.ts +14 -0
  40. package/src/content/content.ts +49 -0
  41. package/src/popup/popup.css +166 -0
  42. package/src/popup/popup.html +43 -0
  43. package/src/popup/popup.ts +113 -0
  44. package/src/shared/i18n/index.ts +29 -0
  45. package/src/shared/i18n/locales.ts +36 -0
  46. package/src/shared/i18n/translations/ar.ts +36 -0
  47. package/src/shared/i18n/translations/de.ts +36 -0
  48. package/src/shared/i18n/translations/en.ts +36 -0
  49. package/src/shared/i18n/translations/es.ts +36 -0
  50. package/src/shared/i18n/translations/fr.ts +36 -0
  51. package/src/shared/i18n/translations/hi.ts +36 -0
  52. package/src/shared/i18n/translations/ja.ts +36 -0
  53. package/src/shared/i18n/translations/pt.ts +36 -0
  54. package/src/shared/i18n/translations/ru.ts +36 -0
  55. package/src/shared/i18n/translations/zh.ts +36 -0
  56. package/src/types/handlebars.d.ts +4 -0
  57. package/tailwind.config.js +39 -0
  58. package/tsconfig.json +18 -0
  59. package/vite.config.ts +67 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(firecrawl scrape:*)",
5
+ "Bash(npx firecrawl:*)",
6
+ "Bash(npm list:*)",
7
+ "WebFetch(domain:ostovjs.org)",
8
+ "Bash(npm install:*)",
9
+ "Bash(npm run:*)",
10
+ "Bash(ls src/app/templates/*.js)",
11
+ "Bash(npm ls:*)",
12
+ "Bash(npm search:*)",
13
+ "Bash(grep 'app.settings.model' src/shared/i18n/translations/*.ts)",
14
+ "WebFetch(domain:platform.openai.com)",
15
+ "WebSearch"
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: ostovjs-expert
3
+ description: Expert skill for building applications with OstovJS (Backbone-like framework with Models, Views, Collections, Events). Use when working with OstovJS, generating components, explaining architecture, handling events, collections, or templates.
4
+ compatibility: Designed for Claude Code and agent environments working with JavaScript/TypeScript projects.
5
+ metadata:
6
+ author: ostovjs
7
+ version: "1.0.0"
8
+ ---
9
+
10
+ # OstovJS Expert
11
+
12
+ You are an expert in OstovJS — a lightweight framework inspired by Backbone.js, based on Models, Views, Collections, and Events.
13
+
14
+ This skill is activated when the user is working with OstovJS or asks about architecture, code generation, or explanations related to it.
15
+
16
+ ---
17
+
18
+ ## 🧠 Core Principles
19
+
20
+ - Separate responsibilities:
21
+ - Models → data and business logic
22
+ - Views → UI rendering
23
+ - Collections → lists of models
24
+ - Events → communication layer
25
+
26
+ - Prefer minimalism and clarity
27
+ - Avoid overengineering
28
+ - Keep code readable and structured
29
+
30
+ ---
31
+
32
+ ## ⚙️ Architecture Rules
33
+
34
+ - Do NOT mix business logic into Views
35
+ - Do NOT place UI logic inside Models
36
+ - Use events instead of direct coupling
37
+ - Prefer composition over inheritance
38
+
39
+ ---
40
+
41
+ ## 🧩 Code Generation
42
+
43
+ When generating code:
44
+
45
+ 1. Use TypeScript by default
46
+ 2. Always generate full working examples (not snippets)
47
+ 3. Include imports and setup
48
+ 4. Use realistic naming:
49
+ - `TodoModel`
50
+ - `TodoView`
51
+ - `TodoCollection`
52
+ 5. Keep structure simple and idiomatic
53
+
54
+ ---
55
+
56
+ ## 🔄 Events
57
+
58
+ Use event-driven architecture:
59
+
60
+ - Prefer loose coupling
61
+ - Show event flow clearly
62
+ - Use patterns like:
63
+ - `on`
64
+ - `emit`
65
+ - `listenTo`
66
+
67
+ ### Example pattern
68
+
69
+ ```ts
70
+ model.on("change", () => {
71
+ view.render()
72
+ })
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # TLDR - AI Page Summarizer
2
+
3
+ A Chrome extension that generates concise TLDR summaries of any web page using OpenAI. Ask follow-up questions in a dedicated tab with full chat interface.
4
+
5
+ ## Features
6
+
7
+ - **Instant TLDR** - One-click page summarization with streaming output
8
+ - **Follow-up Chat** - Ask questions about the page content in a conversational interface
9
+ - **Multi-language** - Summarize in 10 languages: English, Chinese, Spanish, Hindi, Arabic, French, Portuguese, Russian, Japanese, German
10
+ - **Localized UI** - Full interface translation for all supported languages
11
+ - **Model Selection** - Choose from GPT-5.4, GPT-5.4 Mini, GPT-5.4 Nano, GPT-4.1 family, and o3-mini
12
+ - **Streaming** - Responses appear in real-time as they're generated
13
+ - **Auto-summarize** - Opens a dedicated tab and starts summarizing immediately
14
+
15
+ <img width="1724" height="984" alt="Screenshot 2026-03-21 at 22 25 19" src="https://github.com/user-attachments/assets/4ba9c577-ccb8-4d52-a662-92cb3ccdfca4" />
16
+
17
+
18
+ ## Architecture
19
+
20
+ Built with [OstovJS](https://ostovjs.org/) (Backbone-like MVC framework), Handlebars templates (precompiled at build time), and Tailwind CSS with BEM methodology.
21
+
22
+ ```
23
+ src/
24
+ app/ # Main application (opens in new tab)
25
+ models/ # OstovJS Models (Settings, Page, Message, Collection)
26
+ views/ # OstovJS Views (App, Settings, TLDR, Chat, Message, LanguageSelector)
27
+ templates/ # Handlebars templates (.hbs)
28
+ styles/ # Tailwind CSS + BEM (@apply approach)
29
+ services/ # OpenAI API service with streaming
30
+ popup/ # Extension popup (API key + summarize button)
31
+ background/ # Service worker
32
+ content/ # Content script (page text extraction)
33
+ shared/
34
+ i18n/ # Internationalization (10 languages)
35
+ ```
36
+
37
+ ## Setup
38
+
39
+ ```bash
40
+ # Install dependencies
41
+ npm install
42
+
43
+ # Build
44
+ npm run build
45
+
46
+ # Watch mode (rebuild on changes)
47
+ npm run dev
48
+
49
+ # Type check
50
+ npm run typecheck
51
+ ```
52
+
53
+ ## Install in Chrome
54
+
55
+ 1. Run `npm run build`
56
+ 2. Open `chrome://extensions/`
57
+ 3. Enable **Developer mode**
58
+ 4. Click **Load unpacked** and select the `dist/` folder
59
+ 5. Click the extension icon, enter your [OpenAI API key](https://platform.openai.com/api-keys)
60
+ 6. Navigate to any page and click **Summarize**
61
+
62
+ ## Tech Stack
63
+
64
+ - **Framework** - [OstovJS](https://ostovjs.org/) (Models, Views, Collections, Events)
65
+ - **Templates** - Handlebars (precompiled, CSP-safe)
66
+ - **Styling** - Tailwind CSS 3 + BEM + `@apply`
67
+ - **Language** - TypeScript
68
+ - **Bundler** - Vite
69
+ - **API** - OpenAI Chat Completions (streaming)
70
+ - **Platform** - Chrome Extension Manifest V3
71
+
72
+ ## License
73
+
74
+ MIT
package/icons/icon.svg ADDED
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2
+ <rect width="128" height="128" fill="#4c6ef5"/>
3
+ <text x="64" y="82" font-family="system-ui,sans-serif" font-size="56" font-weight="700" fill="#fff" text-anchor="middle">TL</text>
4
+ </svg>
Binary file
Binary file
Binary file
package/manifest.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "TLDR - Page Summarizer",
4
+ "version": "1.0.0",
5
+ "description": "Get a TLDR summary of any page using OpenAI. Ask follow-up questions in a dedicated tab.",
6
+ "permissions": ["activeTab", "storage", "scripting"],
7
+ "action": {
8
+ "default_popup": "src/popup/popup.html",
9
+ "default_icon": {
10
+ "16": "icons/icon16.png",
11
+ "48": "icons/icon48.png",
12
+ "128": "icons/icon128.png"
13
+ }
14
+ },
15
+ "background": {
16
+ "service_worker": "background.js"
17
+ },
18
+ "icons": {
19
+ "16": "icons/icon16.png",
20
+ "48": "icons/icon48.png",
21
+ "128": "icons/icon128.png"
22
+ },
23
+ "content_security_policy": {
24
+ "extension_pages": "script-src 'self'; object-src 'self'"
25
+ }
26
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "tlrd-extension",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "vite build",
10
+ "dev": "vite build --watch",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "handlebars": "^4.7.8",
15
+ "ostovjs": "^1.7.8"
16
+ },
17
+ "devDependencies": {
18
+ "@types/chrome": "^0.1.38",
19
+ "autoprefixer": "^10.4.27",
20
+ "postcss": "^8.5.8",
21
+ "tailwindcss": "^3.4.19",
22
+ "typescript": "^5.9.3",
23
+ "vite": "^8.0.1"
24
+ }
25
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ plugins: {
3
+ 'tailwindcss/nesting': {},
4
+ tailwindcss: {},
5
+ autoprefixer: {},
6
+ },
7
+ };
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>TLDR - Page Summary</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
8
+ </head>
9
+ <body>
10
+ <div id="app" class="app"></div>
11
+ <script type="module" src="app.ts"></script>
12
+ </body>
13
+ </html>
package/src/app/app.ts ADDED
@@ -0,0 +1,50 @@
1
+ import Handlebars from 'handlebars/runtime';
2
+ import { t, setLocale } from '../shared/i18n';
3
+ import type { SupportedLocale } from '../shared/i18n/locales';
4
+ import SettingsModel from './models/SettingsModel';
5
+ import PageModel from './models/PageModel';
6
+ import MessagesCollection from './models/MessagesCollection';
7
+ import AppView from './views/AppView';
8
+ import './styles/input.css';
9
+
10
+ Handlebars.registerHelper('t', (key: string) => t(key));
11
+
12
+ const settingsModel = new SettingsModel();
13
+ const pageModel = new PageModel();
14
+ const messagesCollection = new MessagesCollection();
15
+
16
+ const params = new URLSearchParams(window.location.search);
17
+ const pageTitle = params.get('title') || '';
18
+ const pageUrl = params.get('url') || '';
19
+
20
+ pageModel.set({ title: pageTitle, url: pageUrl }, { silent: true });
21
+
22
+ let appView: AppView | null = null;
23
+ let autoSummarized = false;
24
+
25
+ function tryAutoSummarize() {
26
+ if (autoSummarized || !appView) return;
27
+ if (!settingsModel.hasApiKey() || !pageModel.hasContent()) return;
28
+ autoSummarized = true;
29
+ appView.onSummarize();
30
+ }
31
+
32
+ chrome.storage.session.get('pendingPageContent').then((data) => {
33
+ if (data.pendingPageContent) {
34
+ pageModel.set('content', data.pendingPageContent);
35
+ chrome.storage.session.remove('pendingPageContent');
36
+ tryAutoSummarize();
37
+ }
38
+ });
39
+
40
+ settingsModel.on('loaded', () => {
41
+ setLocale(settingsModel.get('uiLanguage') as SupportedLocale);
42
+
43
+ appView = new AppView({
44
+ settingsModel,
45
+ pageModel,
46
+ messagesCollection,
47
+ });
48
+ appView.render();
49
+ tryAutoSummarize();
50
+ });
@@ -0,0 +1,17 @@
1
+ import { Model } from 'ostovjs';
2
+
3
+ export interface MessageAttributes {
4
+ role: 'user' | 'assistant' | 'system';
5
+ content: string;
6
+ }
7
+
8
+ class MessageModel extends Model<MessageAttributes> {
9
+ defaults() {
10
+ return {
11
+ role: 'user' as const,
12
+ content: '',
13
+ };
14
+ }
15
+ }
16
+
17
+ export default MessageModel;
@@ -0,0 +1,18 @@
1
+ import { Collection } from 'ostovjs';
2
+ import MessageModel, { type MessageAttributes } from './MessageModel';
3
+
4
+ class MessagesCollection extends Collection<MessageModel> {
5
+ constructor(models?: MessageModel[], options?: Record<string, unknown>) {
6
+ super(models, options);
7
+ this.model = MessageModel;
8
+ }
9
+
10
+ toOpenAIFormat(): MessageAttributes[] {
11
+ return this.models.map((msg) => ({
12
+ role: msg.get('role'),
13
+ content: msg.get('content'),
14
+ }));
15
+ }
16
+ }
17
+
18
+ export default MessagesCollection;
@@ -0,0 +1,33 @@
1
+ import { Model } from 'ostovjs';
2
+
3
+ interface PageAttributes {
4
+ title: string;
5
+ url: string;
6
+ content: string;
7
+ tldr: string;
8
+ loading: boolean;
9
+ error: string;
10
+ }
11
+
12
+ class PageModel extends Model<PageAttributes> {
13
+ defaults() {
14
+ return {
15
+ title: '',
16
+ url: '',
17
+ content: '',
18
+ tldr: '',
19
+ loading: false,
20
+ error: '',
21
+ };
22
+ }
23
+
24
+ hasContent(): boolean {
25
+ return Boolean(this.get('content'));
26
+ }
27
+
28
+ hasTldr(): boolean {
29
+ return Boolean(this.get('tldr'));
30
+ }
31
+ }
32
+
33
+ export default PageModel;
@@ -0,0 +1,61 @@
1
+ import { Model } from 'ostovjs';
2
+ import type { SupportedLocale, SummaryLanguage } from '../../shared/i18n/locales';
3
+
4
+ interface SettingsAttributes {
5
+ apiKey: string;
6
+ model: string;
7
+ summaryLanguage: SummaryLanguage;
8
+ uiLanguage: SupportedLocale;
9
+ }
10
+
11
+ export const AVAILABLE_MODELS = [
12
+ { id: 'gpt-5.4', label: 'GPT-5.4' },
13
+ { id: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' },
14
+ { id: 'gpt-5.4-nano', label: 'GPT-5.4 Nano' },
15
+ { id: 'gpt-4.1', label: 'GPT-4.1' },
16
+ { id: 'gpt-4.1-mini', label: 'GPT-4.1 Mini' },
17
+ { id: 'gpt-4.1-nano', label: 'GPT-4.1 Nano' },
18
+ { id: 'o3-mini', label: 'o3-mini' },
19
+ ];
20
+
21
+ class SettingsModel extends Model<SettingsAttributes> {
22
+ defaults() {
23
+ return {
24
+ apiKey: '',
25
+ model: 'gpt-5.4-mini',
26
+ summaryLanguage: 'original' as SummaryLanguage,
27
+ uiLanguage: 'en' as SupportedLocale,
28
+ };
29
+ }
30
+
31
+ initialize() {
32
+ this.load();
33
+ }
34
+
35
+ load(): void {
36
+ chrome.storage.local.get(['apiKey', 'model', 'summaryLanguage', 'uiLanguage']).then((data) => {
37
+ if (data.apiKey) this.set('apiKey', data.apiKey as string, { silent: true });
38
+ if (data.model) this.set('model', data.model as string, { silent: true });
39
+ if (data.summaryLanguage) this.set('summaryLanguage', data.summaryLanguage as SummaryLanguage, { silent: true });
40
+ if (data.uiLanguage) this.set('uiLanguage', data.uiLanguage as SupportedLocale, { silent: true });
41
+ this.trigger('loaded');
42
+ });
43
+ }
44
+
45
+ persist(): void {
46
+ chrome.storage.local.set({
47
+ apiKey: this.get('apiKey'),
48
+ model: this.get('model'),
49
+ summaryLanguage: this.get('summaryLanguage'),
50
+ uiLanguage: this.get('uiLanguage'),
51
+ }).then(() => {
52
+ this.trigger('saved');
53
+ });
54
+ }
55
+
56
+ hasApiKey(): boolean {
57
+ return Boolean(this.get('apiKey'));
58
+ }
59
+ }
60
+
61
+ export default SettingsModel;
@@ -0,0 +1,134 @@
1
+ import type SettingsModel from '../models/SettingsModel';
2
+ import type { MessageAttributes } from '../models/MessageModel';
3
+ import type { SummaryLanguage } from '../../shared/i18n/locales';
4
+ import { LANGUAGE_NAMES } from '../../shared/i18n/locales';
5
+
6
+ function getLanguageInstruction(language: SummaryLanguage): string {
7
+ if (language === 'original') {
8
+ return 'Respond in the same language as the page content.';
9
+ }
10
+ return `Respond in ${LANGUAGE_NAMES[language]}.`;
11
+ }
12
+
13
+ function getTldrSystemPrompt(language: SummaryLanguage): string {
14
+ return `You are a concise summarizer. Given the text content of a web page, produce a clear and informative TLDR summary.
15
+
16
+ If the page is a single article or post: focus on the key points, main arguments, and important details. Aim for 3-6 sentences.
17
+
18
+ If the page is a list of posts, a feed, or an index page: provide a brief overview of what the page contains, mention the main topics and notable items. Don't summarize only the first post — cover the whole page.
19
+
20
+ Use plain language. ${getLanguageInstruction(language)}`;
21
+ }
22
+
23
+ function getChatSystemPrompt(language: SummaryLanguage): string {
24
+ const langInstruction = language === 'original'
25
+ ? 'Respond in the same language as the TLDR summary above.'
26
+ : `Respond in ${LANGUAGE_NAMES[language]}.`;
27
+ return `You are a helpful assistant discussing a web page the user just read. You have access to the page content and its TLDR summary. Answer follow-up questions about the content clearly and concisely. ${langInstruction}`;
28
+ }
29
+
30
+ interface ChatMessage {
31
+ role: string;
32
+ content: string;
33
+ }
34
+
35
+ class OpenAIService {
36
+ private settings: SettingsModel;
37
+
38
+ constructor(settingsModel: SettingsModel) {
39
+ this.settings = settingsModel;
40
+ }
41
+
42
+ async generateTldrStream(
43
+ pageContent: string,
44
+ pageTitle: string,
45
+ language: SummaryLanguage,
46
+ onChunk: (text: string) => void
47
+ ): Promise<string> {
48
+ const messages: ChatMessage[] = [
49
+ { role: 'system', content: getTldrSystemPrompt(language) },
50
+ {
51
+ role: 'user',
52
+ content: `Page title: "${pageTitle}"\n\nPage content:\n${pageContent.slice(0, 12000)}`,
53
+ },
54
+ ];
55
+
56
+ return this._chatStream(messages, onChunk);
57
+ }
58
+
59
+ async askFollowUpStream(
60
+ chatMessages: MessageAttributes[],
61
+ pageContent: string,
62
+ pageTldr: string,
63
+ language: SummaryLanguage,
64
+ onChunk: (text: string) => void
65
+ ): Promise<string> {
66
+ const systemMessage: ChatMessage = {
67
+ role: 'system',
68
+ content: `${getChatSystemPrompt(language)}\n\nPage TLDR:\n${pageTldr}\n\nFull page content (truncated):\n${pageContent.slice(0, 8000)}`,
69
+ };
70
+
71
+ return this._chatStream([systemMessage, ...chatMessages], onChunk);
72
+ }
73
+
74
+ private async _chatStream(
75
+ messages: ChatMessage[],
76
+ onChunk: (text: string) => void
77
+ ): Promise<string> {
78
+ const apiKey = this.settings.get('apiKey');
79
+ if (!apiKey) throw new Error('API key not configured');
80
+
81
+ const model = this.settings.get('model');
82
+
83
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ Authorization: `Bearer ${apiKey}`,
88
+ },
89
+ body: JSON.stringify({ model, messages, stream: true }),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const err = await response.json().catch(() => ({}));
94
+ throw new Error(err.error?.message || `OpenAI API error: ${response.status}`);
95
+ }
96
+
97
+ const reader = response.body!.getReader();
98
+ const decoder = new TextDecoder();
99
+ let fullText = '';
100
+ let buffer = '';
101
+
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+ if (done) break;
105
+
106
+ buffer += decoder.decode(value, { stream: true });
107
+ const lines = buffer.split('\n');
108
+ buffer = lines.pop() || '';
109
+
110
+ for (const line of lines) {
111
+ const trimmed = line.trim();
112
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
113
+
114
+ const data = trimmed.slice(6);
115
+ if (data === '[DONE]') break;
116
+
117
+ try {
118
+ const parsed = JSON.parse(data);
119
+ const delta = parsed.choices?.[0]?.delta?.content;
120
+ if (delta) {
121
+ fullText += delta;
122
+ onChunk(fullText);
123
+ }
124
+ } catch {
125
+ // skip malformed chunks
126
+ }
127
+ }
128
+ }
129
+
130
+ return fullText;
131
+ }
132
+ }
133
+
134
+ export default OpenAIService;
@@ -0,0 +1,38 @@
1
+ @layer components {
2
+ .chat {
3
+ @apply bg-white rounded-2xl border border-surface-200 shadow-sm overflow-hidden;
4
+ }
5
+
6
+ .chat__header {
7
+ @apply px-6 py-4 border-b border-surface-100;
8
+ }
9
+
10
+ .chat__title {
11
+ @apply text-sm font-semibold text-surface-700 uppercase tracking-wider;
12
+ }
13
+
14
+ .chat__messages {
15
+ @apply px-6 py-4 flex flex-col gap-4 max-h-96 overflow-y-auto;
16
+ }
17
+
18
+ .chat__empty {
19
+ @apply text-sm text-surface-400 text-center py-6;
20
+ }
21
+
22
+ .chat__input-wrap {
23
+ @apply px-4 py-3 border-t border-surface-100 flex gap-2;
24
+ }
25
+
26
+ .chat__input {
27
+ @apply flex-1 px-4 py-2.5 rounded-xl border border-surface-300 bg-surface-50 text-sm text-surface-800 placeholder-surface-400
28
+ focus:outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-400 transition-all resize-none;
29
+ }
30
+
31
+ .chat__send-btn {
32
+ @apply btn btn--primary px-4;
33
+ }
34
+
35
+ .chat__send-btn:disabled {
36
+ @apply bg-surface-300 text-surface-500 cursor-not-allowed shadow-none;
37
+ }
38
+ }
@@ -0,0 +1,13 @@
1
+ @import 'tailwindcss/base';
2
+ @import 'tailwindcss/components';
3
+
4
+ @import './layout.css';
5
+ @import './settings.css';
6
+ @import './tldr.css';
7
+ @import './chat.css';
8
+ @import './message.css';
9
+ @import './loader.css';
10
+ @import './scrollbar.css';
11
+ @import './language-selector.css';
12
+
13
+ @import 'tailwindcss/utilities';
@@ -0,0 +1,18 @@
1
+ @layer components {
2
+ .lang-selector {
3
+ @apply flex items-center gap-3;
4
+ }
5
+
6
+ .lang-selector__field {
7
+ @apply flex flex-col gap-1;
8
+ }
9
+
10
+ .lang-selector__label {
11
+ @apply text-[10px] font-medium text-surface-400 uppercase tracking-wider;
12
+ }
13
+
14
+ .lang-selector__select {
15
+ @apply px-2 py-1 text-xs border border-surface-300 rounded-lg bg-white text-surface-700
16
+ focus:outline-none focus:ring-1 focus:ring-primary-200 focus:border-primary-400 cursor-pointer;
17
+ }
18
+ }