agentclick 0.1.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.
@@ -0,0 +1,189 @@
1
+ import 'dotenv/config'
2
+ import express from 'express'
3
+ import cors from 'cors'
4
+ import open from 'open'
5
+ import { existsSync } from 'fs'
6
+ import { dirname, join } from 'path'
7
+ import { fileURLToPath } from 'url'
8
+ import { learnFromDeletions } from './preference.js'
9
+ import { createSession, getSession, listSessions, completeSession } from './store.js'
10
+
11
+ const app = express()
12
+ const PORT = Number(process.env.PORT || 3001)
13
+ const OPENCLAW_WEBHOOK = process.env.OPENCLAW_WEBHOOK || 'http://localhost:18789/hooks/agent'
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const __dirname = dirname(__filename)
16
+ const WEB_DIST_DIR = join(__dirname, '../../web/dist')
17
+ const SHOULD_SERVE_BUILT_WEB = existsSync(WEB_DIST_DIR) && (__filename.endsWith('/dist/index.js') || process.env.NODE_ENV === 'production')
18
+ const WEB_ORIGIN = SHOULD_SERVE_BUILT_WEB ? `http://localhost:${PORT}` : 'http://localhost:5173'
19
+
20
+ app.use(cors())
21
+ app.use(express.json())
22
+
23
+ // OpenClaw calls this when a review is needed
24
+ app.post('/api/review', async (req, res) => {
25
+ const { type, sessionKey, payload } = req.body
26
+
27
+ if (!sessionKey) {
28
+ console.warn('[agentclick] Warning: sessionKey missing — callback will be skipped')
29
+ }
30
+
31
+ const id = `session_${Date.now()}`
32
+ createSession({
33
+ id,
34
+ type: type || 'email_review',
35
+ payload,
36
+ sessionKey,
37
+ status: 'pending',
38
+ createdAt: Date.now(),
39
+ })
40
+
41
+ const routeMap: Record<string, string> = {
42
+ action_approval: 'approval',
43
+ code_review: 'code-review',
44
+ }
45
+ const path = routeMap[type] ?? 'review'
46
+ const url = `${WEB_ORIGIN}/${path}/${id}`
47
+ console.log(`[agentclick] Review session created: ${id}`)
48
+ console.log(`[agentclick] Opening browser: ${url}`)
49
+
50
+ try {
51
+ await open(url)
52
+ } catch (err) {
53
+ console.warn('[agentclick] Failed to open browser:', err)
54
+ }
55
+
56
+ res.json({ sessionId: id, url })
57
+ })
58
+
59
+ // List recent sessions for homepage
60
+ app.get('/api/sessions', (_req, res) => {
61
+ const list = listSessions(20).map(s => {
62
+ const p = s.payload as Record<string, unknown> | undefined
63
+ // Format B (inbox+draft) stores subject/to inside draft
64
+ const draft = p?.draft as Record<string, unknown> | undefined
65
+ return {
66
+ id: s.id,
67
+ type: s.type,
68
+ status: s.status,
69
+ createdAt: s.createdAt,
70
+ subject: (draft?.subject ?? p?.subject) as string | undefined,
71
+ to: (draft?.to ?? p?.to) as string | undefined,
72
+ risk: p?.risk as string | undefined,
73
+ command: p?.command as string | undefined,
74
+ }
75
+ })
76
+ res.json(list)
77
+ })
78
+
79
+ // Web UI fetches session data
80
+ app.get('/api/sessions/:id', (req, res) => {
81
+ const session = getSession(req.params.id)
82
+ if (!session) return res.status(404).json({ error: 'Session not found' })
83
+ res.json(session)
84
+ })
85
+
86
+ // Long-poll: agent calls this and blocks until user completes review (up to 5 min)
87
+ app.get('/api/sessions/:id/wait', async (req, res) => {
88
+ const TIMEOUT_MS = 5 * 60 * 1000
89
+ const POLL_MS = 1500
90
+ const start = Date.now()
91
+
92
+ while (Date.now() - start < TIMEOUT_MS) {
93
+ const session = getSession(req.params.id)
94
+ if (!session) return res.status(404).json({ error: 'Session not found' })
95
+ if (session.status === 'completed') return res.json(session)
96
+ await new Promise(r => setTimeout(r, POLL_MS))
97
+ }
98
+
99
+ res.status(408).json({ error: 'timeout', message: 'User did not complete review within 5 minutes' })
100
+ })
101
+
102
+ // Web UI submits user actions
103
+ app.post('/api/sessions/:id/complete', async (req, res) => {
104
+ const session = getSession(req.params.id)
105
+ if (!session) return res.status(404).json({ error: 'Session not found' })
106
+
107
+ completeSession(req.params.id, req.body)
108
+
109
+ console.log(`[agentclick] Session ${session.id} completed:`, JSON.stringify(req.body, null, 2))
110
+
111
+ // Learn from delete actions and persist rules to MEMORY.md
112
+ const actions = (req.body.actions ?? []) as Array<{ type: string; paragraphId: string; reason?: string; instruction?: string }>
113
+ learnFromDeletions(actions, session.payload as Record<string, unknown>)
114
+
115
+ // Send result back to OpenClaw
116
+ let callbackFailed = false
117
+ let callbackError = ''
118
+
119
+ if (session.sessionKey) {
120
+ try {
121
+ const summary = buildActionSummary(req.body)
122
+ await fetch(OPENCLAW_WEBHOOK, {
123
+ method: 'POST',
124
+ headers: {
125
+ 'Content-Type': 'application/json',
126
+ 'Authorization': `Bearer ${process.env.OPENCLAW_TOKEN || ''}`
127
+ },
128
+ body: JSON.stringify({
129
+ message: summary,
130
+ sessionKey: session.sessionKey,
131
+ deliver: true
132
+ })
133
+ })
134
+ console.log(`[agentclick] Callback sent to OpenClaw`)
135
+ } catch (err) {
136
+ callbackFailed = true
137
+ callbackError = String(err)
138
+ console.error(`[agentclick] Failed to callback OpenClaw:`, err)
139
+ }
140
+ }
141
+
142
+ res.json({ ok: true, callbackFailed, callbackError })
143
+ })
144
+
145
+ function buildActionSummary(result: Record<string, unknown>): string {
146
+ // If result has approved field, it's an action_approval or code_review
147
+ if ('approved' in result) {
148
+ const approved = result.approved as boolean
149
+ const note = result.note as string | undefined
150
+ const lines = ['[agentclick] User reviewed the request:']
151
+ lines.push(approved ? '- Approved: proceed.' : '- Rejected: do not proceed.')
152
+ if (note) lines.push(`- Note: ${note}`)
153
+ return lines.join('\n')
154
+ }
155
+
156
+ const actions = result.actions as Array<{ type: string; paragraphId: string; reason?: string; instruction?: string }> || []
157
+ const lines = ['[agentclick] User reviewed the draft:']
158
+
159
+ const deleted = actions.filter(a => a.type === 'delete')
160
+ const rewritten = actions.filter(a => a.type === 'rewrite')
161
+
162
+ if (deleted.length > 0) {
163
+ lines.push(`- Deleted ${deleted.length} paragraph(s): ${deleted.map(a => `${a.paragraphId} (reason: ${a.reason})`).join(', ')}`)
164
+ }
165
+ if (rewritten.length > 0) {
166
+ lines.push(`- Requested rewrite for: ${rewritten.map(a => `${a.paragraphId} — "${a.instruction}"`).join(', ')}`)
167
+ }
168
+ if (result.confirmed) {
169
+ lines.push('- User confirmed: proceed with sending.')
170
+ }
171
+ if (result.regenerate) {
172
+ lines.push('- User requested full regeneration.')
173
+ }
174
+
175
+ return lines.join('\n')
176
+ }
177
+
178
+ if (SHOULD_SERVE_BUILT_WEB) {
179
+ app.use(express.static(WEB_DIST_DIR))
180
+ app.get('*', (req, res, next) => {
181
+ if (req.path.startsWith('/api/')) return next()
182
+ res.sendFile(join(WEB_DIST_DIR, 'index.html'))
183
+ })
184
+ console.log(`[agentclick] Serving web UI from ${WEB_DIST_DIR}`)
185
+ }
186
+
187
+ app.listen(PORT, () => {
188
+ console.log(`[agentclick] Server running at http://localhost:${PORT}`)
189
+ })
@@ -0,0 +1,101 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ const MEMORY_PATH = path.join(os.homedir(), '.openclaw', 'workspace', 'MEMORY.md')
6
+ const SECTION_HEADER = '## Email Preferences (ClawUI Auto-Learned)'
7
+
8
+ interface Paragraph {
9
+ id: string
10
+ content: string
11
+ }
12
+
13
+ interface SessionPayload {
14
+ type?: string
15
+ paragraphs?: Paragraph[]
16
+ [key: string]: unknown
17
+ }
18
+
19
+ interface ReviewAction {
20
+ type: string
21
+ paragraphId: string
22
+ reason?: string
23
+ instruction?: string
24
+ }
25
+
26
+ // Map raw reason keys to human-readable descriptions
27
+ const REASON_LABELS: Record<string, string> = {
28
+ too_formal: 'too formal',
29
+ too_casual: 'too casual',
30
+ too_long: 'too long',
31
+ off_topic: 'off topic',
32
+ inaccurate: 'inaccurate',
33
+ repetitive: 'repetitive',
34
+ unnecessary: 'unnecessary',
35
+ wrong_tone: 'wrong tone',
36
+ too_polite: 'too polite',
37
+ redundant: 'redundant',
38
+ }
39
+
40
+ function ensureMemoryFile(): void {
41
+ const dir = path.dirname(MEMORY_PATH)
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true })
44
+ }
45
+ if (!fs.existsSync(MEMORY_PATH)) {
46
+ fs.writeFileSync(MEMORY_PATH, '# ClawUI Learned Preferences\n', 'utf-8')
47
+ }
48
+ }
49
+
50
+ // Truncate paragraph content into a short description for the rule
51
+ function summarize(content: string): string {
52
+ const cleaned = content.trim().replace(/\s+/g, ' ')
53
+ if (cleaned.length <= 80) return cleaned
54
+ return cleaned.slice(0, 77) + '...'
55
+ }
56
+
57
+ function resolveReason(reason: string | undefined): string {
58
+ if (!reason) return 'user deleted'
59
+ return REASON_LABELS[reason] ?? reason
60
+ }
61
+
62
+ export function learnFromDeletions(
63
+ actions: ReviewAction[],
64
+ payload: SessionPayload
65
+ ): void {
66
+ const deletions = actions.filter(a => a.type === 'delete')
67
+ if (deletions.length === 0) return
68
+
69
+ const paragraphMap = new Map<string, string>(
70
+ (payload.paragraphs ?? []).map(p => [p.id, p.content])
71
+ )
72
+
73
+ // Infer scope from session type
74
+ const scope = payload.type === 'email_review' ? 'email' : 'general'
75
+
76
+ const rules: string[] = []
77
+ for (const action of deletions) {
78
+ const content = paragraphMap.get(action.paragraphId)
79
+ // Skip if we cannot find the original paragraph text
80
+ if (!content) continue
81
+
82
+ const description = summarize(content)
83
+ const reason = resolveReason(action.reason)
84
+ rules.push(`- AVOID: ${description} (reason: ${reason}) - SCOPE: ${scope}`)
85
+ }
86
+
87
+ if (rules.length === 0) return
88
+
89
+ ensureMemoryFile()
90
+
91
+ const existing = fs.readFileSync(MEMORY_PATH, 'utf-8')
92
+
93
+ // Append section header once if not present, then append rules
94
+ const needsHeader = !existing.includes(SECTION_HEADER)
95
+ const block = needsHeader
96
+ ? `\n${SECTION_HEADER}\n${rules.join('\n')}\n`
97
+ : `${rules.join('\n')}\n`
98
+
99
+ fs.appendFileSync(MEMORY_PATH, block, 'utf-8')
100
+ console.log(`[agentclick] Learned ${rules.length} preference rule(s) -> ${MEMORY_PATH}`)
101
+ }
@@ -0,0 +1,72 @@
1
+ import Database from 'better-sqlite3'
2
+ import { mkdirSync, existsSync } from 'fs'
3
+ import { homedir } from 'os'
4
+ import { join } from 'path'
5
+
6
+ const DB_DIR = join(homedir(), '.openclaw')
7
+ const DB_PATH = join(DB_DIR, 'clawui-sessions.db')
8
+
9
+ if (!existsSync(DB_DIR)) mkdirSync(DB_DIR, { recursive: true })
10
+
11
+ const db = new Database(DB_PATH)
12
+
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS sessions (
15
+ id TEXT PRIMARY KEY,
16
+ type TEXT NOT NULL,
17
+ payload TEXT NOT NULL,
18
+ status TEXT NOT NULL DEFAULT 'pending',
19
+ result TEXT,
20
+ sessionKey TEXT,
21
+ createdAt INTEGER NOT NULL
22
+ )
23
+ `)
24
+
25
+ export interface Session {
26
+ id: string
27
+ type: string
28
+ payload: unknown
29
+ status: 'pending' | 'completed'
30
+ result?: unknown
31
+ sessionKey?: string
32
+ createdAt: number
33
+ }
34
+
35
+ export function createSession(session: Session): void {
36
+ db.prepare(`
37
+ INSERT INTO sessions (id, type, payload, status, sessionKey, createdAt)
38
+ VALUES (@id, @type, @payload, @status, @sessionKey, @createdAt)
39
+ `).run({
40
+ ...session,
41
+ payload: JSON.stringify(session.payload),
42
+ })
43
+ }
44
+
45
+ export function getSession(id: string): Session | null {
46
+ const row = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Record<string, unknown> | undefined
47
+ if (!row) return null
48
+ return deserialize(row)
49
+ }
50
+
51
+ export function listSessions(limit = 20): Session[] {
52
+ const rows = db.prepare('SELECT * FROM sessions ORDER BY createdAt DESC LIMIT ?').all(limit) as Record<string, unknown>[]
53
+ return rows.map(deserialize)
54
+ }
55
+
56
+ export function completeSession(id: string, result: unknown): void {
57
+ db.prepare(`
58
+ UPDATE sessions SET status = 'completed', result = ? WHERE id = ?
59
+ `).run(JSON.stringify(result), id)
60
+ }
61
+
62
+ function deserialize(row: Record<string, unknown>): Session {
63
+ return {
64
+ id: row.id as string,
65
+ type: row.type as string,
66
+ payload: JSON.parse(row.payload as string),
67
+ status: row.status as 'pending' | 'completed',
68
+ result: row.result ? JSON.parse(row.result as string) : undefined,
69
+ sessionKey: row.sessionKey as string | undefined,
70
+ createdAt: row.createdAt as number,
71
+ }
72
+ }
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.right-0{right:0}.top-full{top:100%}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.h-64{height:16rem}.min-h-screen{min-height:100vh}.w-72{width:18rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[148px\]{min-width:148px}.max-w-2xl{max-width:42rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-100{--tw-border-opacity: 1;border-color:rgb(254 243 199 / var(--tw-border-opacity, 1))}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-50{--tw-border-opacity: 1;border-color:rgb(249 250 251 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-l-amber-400{--tw-border-opacity: 1;border-left-color:rgb(251 191 36 / var(--tw-border-opacity, 1))}.border-l-blue-500{--tw-border-opacity: 1;border-left-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-l-red-400{--tw-border-opacity: 1;border-left-color:rgb(248 113 113 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-zinc-100{--tw-bg-opacity: 1;background-color:rgb(244 244 245 / var(--tw-bg-opacity, 1))}.bg-zinc-50{--tw-bg-opacity: 1;background-color:rgb(250 250 250 / var(--tw-bg-opacity, 1))}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.bg-zinc-950{--tw-bg-opacity: 1;background-color:rgb(9 9 11 / var(--tw-bg-opacity, 1))}.p-1\.5{padding:.375rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.pt-0\.5{padding-top:.125rem}.pt-1{padding-top:.25rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-zinc-100{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.text-zinc-300{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.text-zinc-400{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.text-zinc-500{--tw-text-opacity: 1;color:rgb(113 113 122 / var(--tw-text-opacity, 1))}.text-zinc-600{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.text-zinc-700{--tw-text-opacity: 1;color:rgb(63 63 70 / var(--tw-text-opacity, 1))}.text-zinc-800{--tw-text-opacity: 1;color:rgb(39 39 42 / var(--tw-text-opacity, 1))}.text-zinc-900{--tw-text-opacity: 1;color:rgb(24 24 27 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.placeholder-zinc-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(161 161 170 / var(--tw-placeholder-opacity, 1))}.placeholder-zinc-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(161 161 170 / var(--tw-placeholder-opacity, 1))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:border-gray-200:hover{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-700:hover{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.hover\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-zinc-600:hover{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-300:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity, 1))}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.group:hover .group-hover\:flex{display:flex}.group:hover .group-hover\:opacity-100{opacity:1}