cycls 0.0.2.93__py3-none-any.whl
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.
- cycls/__init__.py +14 -0
- cycls/app.py +91 -0
- cycls/auth.py +4 -0
- cycls/cli.py +104 -0
- cycls/function.py +407 -0
- cycls/state.py +6 -0
- cycls/themes/default/assets/index-Xh0IeurI.js +435 -0
- cycls/themes/default/assets/index-oGkkm3Z8.css +1 -0
- cycls/themes/default/index.html +32 -0
- cycls/themes/dev/index.html +298 -0
- cycls/web.py +171 -0
- cycls-0.0.2.93.dist-info/METADATA +282 -0
- cycls-0.0.2.93.dist-info/RECORD +15 -0
- cycls-0.0.2.93.dist-info/WHEEL +4 -0
- cycls-0.0.2.93.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-primary: #ffffff;--bg-secondary: #f9fafb;--bg-tertiary: #f3f4f6;--bg-sidebar: rgba(255, 255, 255, .8);--bg-hover: rgba(0, 0, 0, .05);--bg-active: rgba(0, 0, 0, .07);--bg-overlay: rgba(0, 0, 0, .25);--text-primary: #0d0d0d;--text-secondary: #374151;--text-tertiary: #6b6b6b;--text-muted: #9ca3af;--border-primary: rgba(0, 0, 0, .1);--border-secondary: rgba(0, 0, 0, .06);--accent-primary: #10a37f;--accent-hover: #0d8a6c;--scrollbar-thumb: #d1d1d1;--scrollbar-thumb-hover: #b1b1b1;--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, .05);--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06);--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05);--code-bg: #f3f4f6;--code-text: #1f2937;--msg-user-bg: #f3f4f6;--msg-assistant-bg: transparent;--input-bg: #ffffff;--input-border: #e5e7eb;--input-focus-border: #10a37f;--btn-primary-bg: #0d0d0d;--btn-primary-text: #ffffff;--btn-secondary-bg: transparent;--btn-secondary-text: #0d0d0d}.dark,[data-theme=dark]{--bg-primary: #212121;--bg-secondary: #171717;--bg-tertiary: #2f2f2f;--bg-sidebar: rgba(23, 23, 23, .95);--bg-hover: rgba(255, 255, 255, .08);--bg-active: rgba(255, 255, 255, .12);--bg-overlay: rgba(0, 0, 0, .5);--text-primary: #ececec;--text-secondary: #c5c5c5;--text-tertiary: #8e8e8e;--text-muted: #6b6b6b;--border-primary: rgba(255, 255, 255, .1);--border-secondary: rgba(255, 255, 255, .06);--accent-primary: #10a37f;--accent-hover: #1abc94;--scrollbar-thumb: #4a4a4a;--scrollbar-thumb-hover: #5a5a5a;--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, .3);--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .4), 0 2px 4px -1px rgba(0, 0, 0, .3);--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .4), 0 4px 6px -2px rgba(0, 0, 0, .3);--code-bg: #2f2f2f;--code-text: #e5e7eb;--msg-user-bg: #2f2f2f;--msg-assistant-bg: transparent;--input-bg: #2f2f2f;--input-border: #424242;--input-focus-border: #10a37f;--btn-primary-bg: #ececec;--btn-primary-text: #0d0d0d;--btn-secondary-bg: transparent;--btn-secondary-text: #ececec}@media(prefers-color-scheme:dark){:root:not(.light):not([data-theme=light]){--bg-primary: #212121;--bg-secondary: #171717;--bg-tertiary: #2f2f2f;--bg-sidebar: rgba(23, 23, 23, .95);--bg-hover: rgba(255, 255, 255, .08);--bg-active: rgba(255, 255, 255, .12);--bg-overlay: rgba(0, 0, 0, .5);--text-primary: #ececec;--text-secondary: #c5c5c5;--text-tertiary: #8e8e8e;--text-muted: #6b6b6b;--border-primary: rgba(255, 255, 255, .1);--border-secondary: rgba(255, 255, 255, .06);--accent-primary: #10a37f;--accent-hover: #1abc94;--scrollbar-thumb: #4a4a4a;--scrollbar-thumb-hover: #5a5a5a;--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, .3);--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .4), 0 2px 4px -1px rgba(0, 0, 0, .3);--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .4), 0 4px 6px -2px rgba(0, 0, 0, .3);--code-bg: #2f2f2f;--code-text: #e5e7eb;--msg-user-bg: #2f2f2f;--msg-assistant-bg: transparent;--input-bg: #2f2f2f;--input-border: #424242;--input-focus-border: #10a37f;--btn-primary-bg: #ececec;--btn-primary-text: #0d0d0d;--btn-secondary-bg: transparent;--btn-secondary-text: #ececec}}body{background-color:var(--bg-primary);color:var(--text-primary);transition:background-color .2s ease,color .2s ease}.cl-internal-p8bmz4{box-shadow:none!important;border:1px solid var(--border-primary);box-shadow:var(--shadow-sm)}.cl-drawerRoot{z-index:99!important}.cl-pricingTableCardFooterButton{padding:10px}.cl-pricingTableCardFee{font-size:2rem}.cl-pricingTableCardTitleContainer{margin-bottom:10px}.scrollbar-thin{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}.scrollbar-thin::-webkit-scrollbar{width:6px}.scrollbar-thin::-webkit-scrollbar-track{background:transparent}.scrollbar-thin::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb);border-radius:3px}.scrollbar-thin::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover)}.scrollbar-thin::-webkit-scrollbar-thumb{background-color:transparent}.scrollbar-thin:hover::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb)}.sidebar-transition{transition:transform .3s ease-in-out,width .3s ease-in-out}.sidebar-overlay{z-index:40}.sidebar-panel{z-index:50}@keyframes slideDown{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.dropdown-menu{animation:slideDown .15s ease-out}.sidebar-item:focus-visible{outline:2px solid var(--text-primary);outline-offset:-2px;border-radius:8px}.user-profile-item:hover .user-avatar{transform:scale(1.02)}.chat-title-fade{mask-image:linear-gradient(to right,black 85%,transparent 100%);-webkit-mask-image:linear-gradient(to right,black 85%,transparent 100%)}.theme-toggle-icon{transition:transform .3s ease,opacity .2s ease}.theme-toggle:hover .theme-toggle-icon{transform:rotate(15deg)}.theme-transition{transition:background-color .2s ease,color .2s ease,border-color .2s ease,box-shadow .2s ease}.dark .prose,[data-theme=dark] .prose{--tw-prose-body: var(--text-primary);--tw-prose-headings: var(--text-primary);--tw-prose-lead: var(--text-secondary);--tw-prose-links: var(--accent-primary);--tw-prose-bold: var(--text-primary);--tw-prose-counters: var(--text-tertiary);--tw-prose-bullets: var(--text-tertiary);--tw-prose-hr: var(--border-primary);--tw-prose-quotes: var(--text-secondary);--tw-prose-quote-borders: var(--border-primary);--tw-prose-captions: var(--text-tertiary);--tw-prose-code: var(--text-primary);--tw-prose-pre-code: var(--code-text);--tw-prose-pre-bg: var(--code-bg);--tw-prose-th-borders: var(--border-primary);--tw-prose-td-borders: var(--border-secondary)}@media(prefers-color-scheme:dark){:root:not(.light):not([data-theme=light]) .prose{--tw-prose-body: var(--text-primary);--tw-prose-headings: var(--text-primary);--tw-prose-lead: var(--text-secondary);--tw-prose-links: var(--accent-primary);--tw-prose-bold: var(--text-primary);--tw-prose-counters: var(--text-tertiary);--tw-prose-bullets: var(--text-tertiary);--tw-prose-hr: var(--border-primary);--tw-prose-quotes: var(--text-secondary);--tw-prose-quote-borders: var(--border-primary);--tw-prose-captions: var(--text-tertiary);--tw-prose-code: var(--text-primary);--tw-prose-pre-code: var(--code-text);--tw-prose-pre-bg: var(--code-bg);--tw-prose-th-borders: var(--border-primary);--tw-prose-td-borders: var(--border-secondary)}}.dark pre code.hljs,[data-theme=dark] pre code.hljs{background:var(--code-bg)!important}@media(prefers-color-scheme:dark){:root:not(.light):not([data-theme=light]) pre code.hljs{background:var(--code-bg)!important}}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>AI Agent</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<!-- Default SEO tags - dynamically updated by SEOHead component -->
|
|
8
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
9
|
+
<meta name="googlebot" content="noindex, nofollow" />
|
|
10
|
+
<meta name="description" content="AI-powered chat interface" />
|
|
11
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
|
|
12
|
+
<link
|
|
13
|
+
rel="stylesheet"
|
|
14
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css"
|
|
15
|
+
/>
|
|
16
|
+
<link
|
|
17
|
+
rel="stylesheet"
|
|
18
|
+
href="https://esm.sh/katex@0.16.8/dist/katex.min.css"
|
|
19
|
+
/>
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-Xh0IeurI.js"></script>
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-oGkkm3Z8.css">
|
|
22
|
+
</head>
|
|
23
|
+
<body style="overflow-x: hidden">
|
|
24
|
+
<div id="root"></div>
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
tailwind.config = {
|
|
28
|
+
darkMode: "class",
|
|
29
|
+
};
|
|
30
|
+
</script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Spark - Native Components</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
9
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
tailwind.config = { darkMode: "class" };
|
|
13
|
+
</script>
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--bg-primary: #ffffff;
|
|
17
|
+
--bg-secondary: #f9fafb;
|
|
18
|
+
--text-primary: #0d0d0d;
|
|
19
|
+
--text-secondary: #6b7280;
|
|
20
|
+
--border-color: #e5e7eb;
|
|
21
|
+
--accent: #10a37f;
|
|
22
|
+
}
|
|
23
|
+
.dark {
|
|
24
|
+
--bg-primary: #212121;
|
|
25
|
+
--bg-secondary: #171717;
|
|
26
|
+
--text-primary: #ececec;
|
|
27
|
+
--text-secondary: #9ca3af;
|
|
28
|
+
--border-color: #374151;
|
|
29
|
+
--accent: #10a37f;
|
|
30
|
+
}
|
|
31
|
+
body {
|
|
32
|
+
background: var(--bg-primary);
|
|
33
|
+
color: var(--text-primary);
|
|
34
|
+
}
|
|
35
|
+
.thinking-bubble {
|
|
36
|
+
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
|
37
|
+
border-left: 3px solid var(--accent);
|
|
38
|
+
}
|
|
39
|
+
.dark .thinking-bubble {
|
|
40
|
+
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
|
|
41
|
+
}
|
|
42
|
+
.callout-info { border-left-color: #3b82f6; background: #eff6ff; }
|
|
43
|
+
.callout-warning { border-left-color: #f59e0b; background: #fffbeb; }
|
|
44
|
+
.callout-error { border-left-color: #ef4444; background: #fef2f2; }
|
|
45
|
+
.callout-success { border-left-color: #10b981; background: #ecfdf5; }
|
|
46
|
+
.dark .callout-info { background: #1e3a5f; }
|
|
47
|
+
.dark .callout-warning { background: #422006; }
|
|
48
|
+
.dark .callout-error { background: #450a0a; }
|
|
49
|
+
.dark .callout-success { background: #064e3b; }
|
|
50
|
+
@keyframes pulse-border {
|
|
51
|
+
0%, 100% { border-color: var(--accent); }
|
|
52
|
+
50% { border-color: transparent; }
|
|
53
|
+
}
|
|
54
|
+
.streaming { animation: pulse-border 1s infinite; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body class="dark">
|
|
58
|
+
<div id="app" class="min-h-screen flex flex-col">
|
|
59
|
+
<!-- Header -->
|
|
60
|
+
<header class="border-b border-[var(--border-color)] p-4">
|
|
61
|
+
<div class="max-w-3xl mx-auto flex items-center justify-between">
|
|
62
|
+
<h1 class="text-xl font-semibold">Spark</h1>
|
|
63
|
+
<button onclick="toggleDark()" class="p-2 rounded hover:bg-[var(--bg-secondary)]">
|
|
64
|
+
<span id="theme-icon">🌙</span>
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
<!-- Messages -->
|
|
70
|
+
<main class="flex-1 overflow-y-auto p-4">
|
|
71
|
+
<div id="messages" class="max-w-3xl mx-auto space-y-4"></div>
|
|
72
|
+
</main>
|
|
73
|
+
|
|
74
|
+
<!-- Input -->
|
|
75
|
+
<footer class="border-t border-[var(--border-color)] p-4">
|
|
76
|
+
<form id="chat-form" class="max-w-3xl mx-auto flex gap-2">
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
id="input"
|
|
80
|
+
placeholder="Send a message..."
|
|
81
|
+
class="flex-1 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] px-4 py-3 text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
|
|
82
|
+
/>
|
|
83
|
+
<button
|
|
84
|
+
type="submit"
|
|
85
|
+
class="rounded-lg bg-[var(--accent)] px-6 py-3 text-white font-medium hover:opacity-90"
|
|
86
|
+
>
|
|
87
|
+
Send
|
|
88
|
+
</button>
|
|
89
|
+
</form>
|
|
90
|
+
</footer>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<script>
|
|
94
|
+
// State
|
|
95
|
+
let messages = [];
|
|
96
|
+
let isDark = true;
|
|
97
|
+
|
|
98
|
+
// Toggle dark mode
|
|
99
|
+
function toggleDark() {
|
|
100
|
+
isDark = !isDark;
|
|
101
|
+
document.body.classList.toggle('dark', isDark);
|
|
102
|
+
document.getElementById('theme-icon').textContent = isDark ? '🌙' : '☀️';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Native component renderers
|
|
106
|
+
const components = {
|
|
107
|
+
text: (props) => marked.parse(props.text || '', { breaks: true }),
|
|
108
|
+
|
|
109
|
+
thinking: (props) => `
|
|
110
|
+
<div class="thinking-bubble rounded-lg p-4 my-3 italic text-[var(--text-secondary)]">
|
|
111
|
+
<div class="flex items-center gap-2 mb-2 text-sm font-medium text-[var(--accent)]">
|
|
112
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
113
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
114
|
+
</svg>
|
|
115
|
+
Thinking
|
|
116
|
+
</div>
|
|
117
|
+
<div>${props.thinking}</div>
|
|
118
|
+
</div>
|
|
119
|
+
`,
|
|
120
|
+
|
|
121
|
+
table: (props) => `
|
|
122
|
+
<div class="overflow-x-auto my-3">
|
|
123
|
+
<table class="min-w-full border border-[var(--border-color)] rounded-lg overflow-hidden">
|
|
124
|
+
${props.headers ? `
|
|
125
|
+
<thead class="bg-[var(--bg-secondary)]">
|
|
126
|
+
<tr>
|
|
127
|
+
${props.headers.map(h => `<th class="px-4 py-2 text-left font-medium">${h}</th>`).join('')}
|
|
128
|
+
</tr>
|
|
129
|
+
</thead>
|
|
130
|
+
` : ''}
|
|
131
|
+
<tbody>
|
|
132
|
+
${(props.rows || []).map((row, i) => `
|
|
133
|
+
<tr class="${i % 2 ? 'bg-[var(--bg-secondary)]' : ''}">
|
|
134
|
+
${row.map(cell => `<td class="px-4 py-2 border-t border-[var(--border-color)]">${cell}</td>`).join('')}
|
|
135
|
+
</tr>
|
|
136
|
+
`).join('')}
|
|
137
|
+
</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
</div>
|
|
140
|
+
`,
|
|
141
|
+
|
|
142
|
+
code: (props) => {
|
|
143
|
+
const highlighted = props.language
|
|
144
|
+
? hljs.highlight(props.code, { language: props.language }).value
|
|
145
|
+
: hljs.highlightAuto(props.code).value;
|
|
146
|
+
return `
|
|
147
|
+
<div class="my-3 rounded-lg overflow-hidden border border-[var(--border-color)]">
|
|
148
|
+
<div class="bg-[var(--bg-secondary)] px-4 py-2 text-xs text-[var(--text-secondary)] flex justify-between items-center">
|
|
149
|
+
<span>${props.language || 'code'}</span>
|
|
150
|
+
<button onclick="copyCode(this)" class="hover:text-[var(--accent)]">Copy</button>
|
|
151
|
+
</div>
|
|
152
|
+
<pre class="p-4 overflow-x-auto bg-[#0d1117]"><code class="text-sm">${highlighted}</code></pre>
|
|
153
|
+
</div>
|
|
154
|
+
`;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
callout: (props) => `
|
|
158
|
+
<div class="callout-${props.style || 'info'} border-l-4 rounded-r-lg p-4 my-3">
|
|
159
|
+
${props.title ? `<div class="font-semibold mb-1">${props.title}</div>` : ''}
|
|
160
|
+
<div class="text-sm">${props.callout}</div>
|
|
161
|
+
</div>
|
|
162
|
+
`,
|
|
163
|
+
|
|
164
|
+
image: (props) => `
|
|
165
|
+
<div class="my-3">
|
|
166
|
+
<img src="${props.src}" alt="${props.alt || ''}" class="rounded-lg max-w-full" />
|
|
167
|
+
${props.caption ? `<p class="text-sm text-[var(--text-secondary)] mt-2 text-center">${props.caption}</p>` : ''}
|
|
168
|
+
</div>
|
|
169
|
+
`,
|
|
170
|
+
|
|
171
|
+
button: (props) => `
|
|
172
|
+
<button
|
|
173
|
+
onclick="handleComponentAction('${props.action || ''}', ${JSON.stringify(props.payload || {})})"
|
|
174
|
+
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--accent)] text-white font-medium hover:opacity-90 my-2"
|
|
175
|
+
>
|
|
176
|
+
${props.label}
|
|
177
|
+
</button>
|
|
178
|
+
`
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Copy code helper
|
|
182
|
+
function copyCode(btn) {
|
|
183
|
+
const code = btn.closest('.rounded-lg').querySelector('code').textContent;
|
|
184
|
+
navigator.clipboard.writeText(code);
|
|
185
|
+
btn.textContent = 'Copied!';
|
|
186
|
+
setTimeout(() => btn.textContent = 'Copy', 2000);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Component action handler (for interactive components)
|
|
190
|
+
function handleComponentAction(action, payload) {
|
|
191
|
+
console.log('Component action:', action, payload);
|
|
192
|
+
// Could send back to server or handle locally
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Render a single message
|
|
196
|
+
function renderMessage(msg) {
|
|
197
|
+
const wrapper = document.createElement('div');
|
|
198
|
+
wrapper.className = `rounded-lg p-4 ${msg.role === 'user'
|
|
199
|
+
? 'bg-[var(--accent)] text-white ml-12'
|
|
200
|
+
: 'bg-[var(--bg-secondary)] mr-12'}`;
|
|
201
|
+
|
|
202
|
+
if (msg.role === 'user') {
|
|
203
|
+
wrapper.innerHTML = `<div>${msg.content}</div>`;
|
|
204
|
+
} else {
|
|
205
|
+
let html = '';
|
|
206
|
+
for (const part of msg.parts || [])
|
|
207
|
+
html += components[part.type]?.(part) || '';
|
|
208
|
+
wrapper.innerHTML = `<div class="prose prose-invert max-w-none">${html}</div>`;
|
|
209
|
+
}
|
|
210
|
+
return wrapper;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Render all messages
|
|
214
|
+
function render() {
|
|
215
|
+
const container = document.getElementById('messages');
|
|
216
|
+
container.innerHTML = '';
|
|
217
|
+
messages.forEach(msg => container.appendChild(renderMessage(msg)));
|
|
218
|
+
container.scrollTop = container.scrollHeight;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Stream response from server
|
|
222
|
+
async function streamResponse(userMessage) {
|
|
223
|
+
messages.push({ role: 'user', content: userMessage });
|
|
224
|
+
messages.push({ role: 'assistant', parts: [] });
|
|
225
|
+
render();
|
|
226
|
+
|
|
227
|
+
const response = await fetch('/chat/cycls', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
messages: messages.slice(0, -1).map(m => ({ role: m.role, content: m.content, parts: m.parts }))
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const reader = response.body.getReader();
|
|
236
|
+
const decoder = new TextDecoder();
|
|
237
|
+
let buffer = '';
|
|
238
|
+
let assistantMsg = messages[messages.length - 1];
|
|
239
|
+
let currentPart = null;
|
|
240
|
+
|
|
241
|
+
while (true) {
|
|
242
|
+
const { done, value } = await reader.read();
|
|
243
|
+
if (done) break;
|
|
244
|
+
|
|
245
|
+
buffer += decoder.decode(value, { stream: true });
|
|
246
|
+
const lines = buffer.split('\n');
|
|
247
|
+
buffer = lines.pop() || '';
|
|
248
|
+
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
if (!line.startsWith('data: ')) continue;
|
|
251
|
+
const data = line.slice(6);
|
|
252
|
+
if (data === '[DONE]') continue;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const item = JSON.parse(data);
|
|
256
|
+
const type = item.type;
|
|
257
|
+
|
|
258
|
+
// Same type as current? Append content
|
|
259
|
+
if (currentPart && currentPart.type === type) {
|
|
260
|
+
if (item.row) currentPart.rows.push(item.row);
|
|
261
|
+
else if (item[type]) currentPart[type] = (currentPart[type] || '') + item[type];
|
|
262
|
+
} else {
|
|
263
|
+
// New component
|
|
264
|
+
currentPart = { ...item };
|
|
265
|
+
if (item.headers) currentPart.rows = [];
|
|
266
|
+
assistantMsg.parts.push(currentPart);
|
|
267
|
+
}
|
|
268
|
+
render();
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error('Parse error:', e, data);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
assistantMsg.parts = assistantMsg.parts.filter(p =>
|
|
275
|
+
p.type !== 'text' || p.text?.trim()
|
|
276
|
+
);
|
|
277
|
+
render();
|
|
278
|
+
console.log(messages);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Form submit
|
|
282
|
+
document.getElementById('chat-form').addEventListener('submit', async (e) => {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
const input = document.getElementById('input');
|
|
285
|
+
const message = input.value.trim();
|
|
286
|
+
if (!message) return;
|
|
287
|
+
input.value = '';
|
|
288
|
+
await streamResponse(message);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Highlight code blocks after render
|
|
292
|
+
const observer = new MutationObserver(() => {
|
|
293
|
+
document.querySelectorAll('pre code:not(.hljs)').forEach(el => hljs.highlightElement(el));
|
|
294
|
+
});
|
|
295
|
+
observer.observe(document.getElementById('messages'), { childList: true, subtree: true });
|
|
296
|
+
</script>
|
|
297
|
+
</body>
|
|
298
|
+
</html>
|
cycls/web.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import json, inspect
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional, Union, Any
|
|
5
|
+
from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
|
|
6
|
+
|
|
7
|
+
class Config(BaseModel):
|
|
8
|
+
public_path: str = "theme"
|
|
9
|
+
header: Optional[str] = None
|
|
10
|
+
intro: Optional[str] = None
|
|
11
|
+
title: Optional[str] = None
|
|
12
|
+
prod: bool = False
|
|
13
|
+
auth: bool = False
|
|
14
|
+
plan: str = "free"
|
|
15
|
+
analytics: bool = False
|
|
16
|
+
org: Optional[str] = None
|
|
17
|
+
pk: Optional[str] = None
|
|
18
|
+
jwks: Optional[str] = None
|
|
19
|
+
state: Union[bool, str] = False
|
|
20
|
+
|
|
21
|
+
def set_prod(self, prod: bool):
|
|
22
|
+
self.prod = prod
|
|
23
|
+
self.pk = PK_LIVE if prod else PK_TEST
|
|
24
|
+
self.jwks = JWKS_PROD if prod else JWKS_TEST
|
|
25
|
+
|
|
26
|
+
async def openai_encoder(stream):
|
|
27
|
+
if inspect.isasyncgen(stream):
|
|
28
|
+
async for msg in stream:
|
|
29
|
+
if msg: yield f"data: {json.dumps({'choices': [{'delta': {'content': msg}}]})}\n\n"
|
|
30
|
+
else:
|
|
31
|
+
for msg in stream:
|
|
32
|
+
if msg: yield f"data: {json.dumps({'choices': [{'delta': {'content': msg}}]})}\n\n"
|
|
33
|
+
yield "data: [DONE]\n\n"
|
|
34
|
+
|
|
35
|
+
def sse(item):
|
|
36
|
+
if not item: return None
|
|
37
|
+
if not isinstance(item, dict): item = {"type": "text", "text": item}
|
|
38
|
+
return f"data: {json.dumps(item)}\n\n"
|
|
39
|
+
|
|
40
|
+
async def encoder(stream):
|
|
41
|
+
if inspect.isasyncgen(stream):
|
|
42
|
+
async for item in stream:
|
|
43
|
+
if msg := sse(item): yield msg
|
|
44
|
+
else:
|
|
45
|
+
for item in stream:
|
|
46
|
+
if msg := sse(item): yield msg
|
|
47
|
+
yield "data: [DONE]\n\n"
|
|
48
|
+
|
|
49
|
+
class Messages(list):
|
|
50
|
+
"""A list that provides text-only messages by default, with .raw for full data."""
|
|
51
|
+
def __init__(self, raw_messages):
|
|
52
|
+
self._raw = raw_messages
|
|
53
|
+
text_messages = []
|
|
54
|
+
for m in raw_messages:
|
|
55
|
+
text_content = "".join(
|
|
56
|
+
p.get("text", "") for p in m.get("parts", []) if p.get("type") == "text"
|
|
57
|
+
)
|
|
58
|
+
text_messages.append({
|
|
59
|
+
"role": m.get("role"),
|
|
60
|
+
"content": m.get("content") or text_content
|
|
61
|
+
})
|
|
62
|
+
super().__init__(text_messages)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def raw(self):
|
|
66
|
+
return self._raw
|
|
67
|
+
|
|
68
|
+
def web(func, config):
|
|
69
|
+
from fastapi import FastAPI, Request, HTTPException, status, Depends
|
|
70
|
+
from fastapi.responses import StreamingResponse
|
|
71
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
72
|
+
import jwt
|
|
73
|
+
from jwt import PyJWKClient
|
|
74
|
+
from pydantic import EmailStr
|
|
75
|
+
from typing import List, Optional, Any
|
|
76
|
+
from fastapi.staticfiles import StaticFiles
|
|
77
|
+
|
|
78
|
+
if isinstance(config, dict):
|
|
79
|
+
config = Config(**config)
|
|
80
|
+
|
|
81
|
+
jwks = PyJWKClient(config.jwks)
|
|
82
|
+
|
|
83
|
+
class User(BaseModel):
|
|
84
|
+
id: str
|
|
85
|
+
name: Optional[str] = None
|
|
86
|
+
email: EmailStr
|
|
87
|
+
org: Optional[str] = None
|
|
88
|
+
plans: List[str] = []
|
|
89
|
+
|
|
90
|
+
class Context(BaseModel):
|
|
91
|
+
messages: Any
|
|
92
|
+
user: Optional[User] = None
|
|
93
|
+
state: Optional[Any] = None
|
|
94
|
+
|
|
95
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def last_message(self) -> str:
|
|
99
|
+
if self.messages:
|
|
100
|
+
return self.messages[-1].get("content", "")
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def kv(self):
|
|
105
|
+
return self.state.kv if self.state else None
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def fs(self):
|
|
109
|
+
return self.state.fs if self.state else None
|
|
110
|
+
|
|
111
|
+
app = FastAPI()
|
|
112
|
+
bearer_scheme = HTTPBearer()
|
|
113
|
+
|
|
114
|
+
def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
|
|
115
|
+
try:
|
|
116
|
+
key = jwks.get_signing_key_from_jwt(bearer.credentials)
|
|
117
|
+
decoded = jwt.decode(bearer.credentials, key.key, algorithms=["RS256"], leeway=10)
|
|
118
|
+
return {"type": "user",
|
|
119
|
+
"user": {"id": decoded.get("id"), "name": decoded.get("name"), "email": decoded.get("email"), "org": decoded.get("org"),
|
|
120
|
+
"plans": decoded.get("public", {}).get("plans", [])}}
|
|
121
|
+
except jwt.ExpiredSignatureError:
|
|
122
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired", headers={"WWW-Authenticate": "Bearer"})
|
|
123
|
+
except jwt.InvalidTokenError as e:
|
|
124
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid token: {e}", headers={"WWW-Authenticate": "Bearer"})
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Auth error: {e}", headers={"WWW-Authenticate": "Bearer"})
|
|
127
|
+
|
|
128
|
+
@app.post("/")
|
|
129
|
+
@app.post("/chat/cycls")
|
|
130
|
+
@app.post("/chat/completions")
|
|
131
|
+
async def back(request: Request, jwt: Optional[dict] = Depends(validate) if config.auth else None):
|
|
132
|
+
data = await request.json()
|
|
133
|
+
messages = data.get("messages")
|
|
134
|
+
user_data = jwt.get("user") if jwt else None
|
|
135
|
+
user = User(**user_data) if user_data else None
|
|
136
|
+
|
|
137
|
+
# Initialize state scoped to user
|
|
138
|
+
state_instance = None
|
|
139
|
+
if config.state:
|
|
140
|
+
from .state import create_state
|
|
141
|
+
user_id = user.id if user else "anonymous"
|
|
142
|
+
state_instance = await create_state(user_id)
|
|
143
|
+
|
|
144
|
+
context = Context(messages=Messages(messages), user=user, state=state_instance)
|
|
145
|
+
stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
|
|
146
|
+
|
|
147
|
+
if request.url.path == "/chat/completions":
|
|
148
|
+
stream = openai_encoder(stream)
|
|
149
|
+
elif request.url.path == "/chat/cycls":
|
|
150
|
+
stream = encoder(stream)
|
|
151
|
+
return StreamingResponse(stream, media_type="text/event-stream")
|
|
152
|
+
|
|
153
|
+
@app.get("/config")
|
|
154
|
+
async def get_config():
|
|
155
|
+
return config
|
|
156
|
+
|
|
157
|
+
if Path("public").is_dir():
|
|
158
|
+
app.mount("/public", StaticFiles(directory="public", html=True))
|
|
159
|
+
app.mount("/", StaticFiles(directory=config.public_path, html=True))
|
|
160
|
+
|
|
161
|
+
return app
|
|
162
|
+
|
|
163
|
+
def serve(func, config, name, port):
|
|
164
|
+
import uvicorn, logging
|
|
165
|
+
from dotenv import load_dotenv
|
|
166
|
+
load_dotenv()
|
|
167
|
+
if isinstance(config, dict):
|
|
168
|
+
config = Config(**config)
|
|
169
|
+
logging.getLogger("uvicorn.error").addFilter(lambda r: "0.0.0.0" not in r.getMessage())
|
|
170
|
+
print(f"\n🔨 {name} => http://localhost:{port}\n")
|
|
171
|
+
uvicorn.run(web(func, config), host="0.0.0.0", port=port)
|