asasvirtuais 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.
- package/README.md +78 -0
- package/actions/draw.ts +110 -0
- package/components/OAuthCard.tsx +346 -0
- package/components/icons.tsx +11 -0
- package/components/markdown.tsx +18 -0
- package/components/stack/list.tsx +21 -0
- package/components/stack/menu.tsx +40 -0
- package/components/stack/nav.tsx +39 -0
- package/components/table/fixed.tsx +59 -0
- package/components/table/key-value.tsx +19 -0
- package/components/ui/color-mode.tsx +108 -0
- package/components/ui/provider.tsx +15 -0
- package/components/ui/toaster.tsx +43 -0
- package/components/ui/tooltip.tsx +46 -0
- package/hooks/useBoolean.tsx +11 -0
- package/hooks/useForwardAs.tsx +29 -0
- package/hooks/useHash copy.tsx +27 -0
- package/hooks/useHash.tsx +27 -0
- package/hooks/useIsMobile.tsx +6 -0
- package/hooks/useOAuthTokens.ts +97 -0
- package/hooks/useOpenRouterModels.ts +80 -0
- package/lib/auth0.ts +11 -0
- package/lib/blob.ts +3 -0
- package/lib/client-token-storage.ts +216 -0
- package/lib/oauth-tokens.ts +85 -0
- package/lib/react/context.tsx +20 -0
- package/lib/react/index.ts +1 -0
- package/lib/tools.ts +375 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +23 -0
- package/package.json +72 -0
- package/packages/blob.ts +97 -0
- package/packages/chat/components/chat/feed/index.tsx +76 -0
- package/packages/chat/components/chat/feed/story.tsx +18 -0
- package/packages/chat/components/chat/index.tsx +16 -0
- package/packages/chat/components/chat/story.tsx +74 -0
- package/packages/chat/components/debug/index.tsx +54 -0
- package/packages/chat/components/header/index.tsx +14 -0
- package/packages/chat/components/header/menu/index.tsx +63 -0
- package/packages/chat/components/header/story.tsx +33 -0
- package/packages/chat/components/header/title/index.tsx +35 -0
- package/packages/chat/components/index.ts +13 -0
- package/packages/chat/components/input/index.tsx +17 -0
- package/packages/chat/components/input/menu/index.tsx +35 -0
- package/packages/chat/components/input/send.tsx +21 -0
- package/packages/chat/components/input/story.tsx +35 -0
- package/packages/chat/components/input/textarea/index.tsx +20 -0
- package/packages/chat/components/message/file.tsx +103 -0
- package/packages/chat/components/message/menu/index.tsx +26 -0
- package/packages/chat/components/message/story.tsx +49 -0
- package/packages/chat/components/messages/index.tsx +23 -0
- package/packages/chat/components/messages/story.tsx +11 -0
- package/packages/chat/components/ui/prose.tsx +263 -0
- package/packages/chat/edit-message.tsx +49 -0
- package/packages/chat/header.tsx +118 -0
- package/packages/chat/index.ts +14 -0
- package/packages/chat/input.tsx +89 -0
- package/packages/chat/message-menu.tsx +57 -0
- package/packages/chat/message.tsx +44 -0
- package/packages/chat/messages.tsx +44 -0
- package/packages/chat/model-selector.tsx +172 -0
- package/packages/chat/scenarios.tsx +68 -0
- package/packages/chat/settings.tsx +98 -0
- package/packages/chat/temperature-slider.tsx +67 -0
- package/packages/chat/tool-results.tsx +32 -0
- package/packages/crud/core.ts +75 -0
- package/packages/crud/fetcher.ts +64 -0
- package/packages/crud/index.ts +2 -0
- package/packages/crud/next.ts +128 -0
- package/packages/crud/react.tsx +365 -0
- package/packages/env.ts +8 -0
- package/packages/fields.tsx +157 -0
- package/packages/firebase.ts +13 -0
- package/packages/firestore.ts +51 -0
- package/packages/form.tsx +66 -0
- package/packages/next.ts +64 -0
- package/packages/openrouter.ts +4 -0
- package/packages/react/context.tsx +21 -0
- package/packages/react/crud.tsx +372 -0
- package/packages/react/hooks.ts +90 -0
- package/packages/react/store.tsx +20 -0
- package/packages/replit-db.ts +219 -0
- package/packages/wretch.ts +22 -0
- package/packages/yaml.ts +163 -0
- package/pnpm-workspace.yaml +4 -0
- package/server/db.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
Database Schema
|
|
3
|
+
```tsx
|
|
4
|
+
const schema = {
|
|
5
|
+
users: {
|
|
6
|
+
readable: {
|
|
7
|
+
id: z.string(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
},
|
|
11
|
+
writable: {
|
|
12
|
+
name: z.string(),
|
|
13
|
+
email: z.string().email(),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Fields
|
|
20
|
+
```tsx
|
|
21
|
+
import { fields as modules } from '@/packages/fields'
|
|
22
|
+
const fields = {
|
|
23
|
+
// Only writable fields
|
|
24
|
+
users: {
|
|
25
|
+
name: modules.string,
|
|
26
|
+
email: modules.email,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
TODO: REST API Route
|
|
32
|
+
```tsx
|
|
33
|
+
import { route } from '@/packages/next'
|
|
34
|
+
export default route<{ id: string }>(
|
|
35
|
+
async function(request, params) {
|
|
36
|
+
const [payload, { id }] = await Promise.all([request.json(), params])
|
|
37
|
+
return Response.json(
|
|
38
|
+
routeLogic(id, payload)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
CreateForm, UpdateForm and FiltersForm
|
|
45
|
+
```tsx
|
|
46
|
+
function MyFormGoesHere() {
|
|
47
|
+
return (
|
|
48
|
+
<FiltersForm table={} onSuccess={handleResult}>
|
|
49
|
+
{/* TODO: handle forms with function as child */}
|
|
50
|
+
{({loading, result, error}) => {
|
|
51
|
+
return (
|
|
52
|
+
<Stack>
|
|
53
|
+
<FiltersFields/>
|
|
54
|
+
{/* TODO: SubmitButton with loading spinner */}
|
|
55
|
+
<SubmitButton disabled={loading} loading={loading}>
|
|
56
|
+
{loading ? <Spinner/> : 'Apply Filters'}
|
|
57
|
+
</SubmitButton>
|
|
58
|
+
{result.map(item => <ViewItem key={item.id} item={item} />)}
|
|
59
|
+
</Stack>
|
|
60
|
+
)
|
|
61
|
+
}}
|
|
62
|
+
</FiltersForm>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Using the DatabaseProvider
|
|
68
|
+
```tsx
|
|
69
|
+
const index = Object.fromEntries((await server.users.list({})).map(user => [user.id, user]))
|
|
70
|
+
<DatabaseProvider users={index}>
|
|
71
|
+
{...}
|
|
72
|
+
</DatabaseProvider>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Listing Query
|
|
76
|
+
```tsx
|
|
77
|
+
const array = await server.messages.list({query: { id }})
|
|
78
|
+
```
|
package/actions/draw.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
import JSZip from 'jszip'
|
|
3
|
+
import { uploadBuffer } from '@/packages/blob'
|
|
4
|
+
|
|
5
|
+
const NOVELAI_API_URL = 'https://image.novelai.net/ai/generate-image'
|
|
6
|
+
const NOVELAI_API_TOKEN = process.env.NOVELAI_API_TOKEN
|
|
7
|
+
|
|
8
|
+
export interface DrawParams {
|
|
9
|
+
prompt?: string
|
|
10
|
+
orientation?: 'portrait' | 'landscape' | 'square'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RESOLUTION_MAP = {
|
|
14
|
+
portrait: { width: 832, height: 1216 },
|
|
15
|
+
landscape: { width: 1216, height: 832 },
|
|
16
|
+
square: { width: 1024, height: 1024 }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function drawAction({ prompt, orientation = 'square' }: DrawParams) {
|
|
20
|
+
// Following the exact NovelAI API format from the website
|
|
21
|
+
|
|
22
|
+
const enhancedPrompt = `masterpiece, best quality, ${prompt}`
|
|
23
|
+
|
|
24
|
+
const { width = 1024, height = 1024 } = RESOLUTION_MAP[orientation]
|
|
25
|
+
|
|
26
|
+
const payload = {
|
|
27
|
+
input: enhancedPrompt,
|
|
28
|
+
model: 'nai-diffusion-4-5-full',
|
|
29
|
+
action: "generate",
|
|
30
|
+
parameters: {
|
|
31
|
+
params_version: 3,
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
scale: 5,
|
|
35
|
+
sampler: 'k_euler_ancestral',
|
|
36
|
+
steps: 28,
|
|
37
|
+
n_samples: 1,
|
|
38
|
+
ucPreset: 4,
|
|
39
|
+
qualityToggle: false,
|
|
40
|
+
autoSmea: false,
|
|
41
|
+
dynamic_thresholding: false,
|
|
42
|
+
controlnet_strength: 1,
|
|
43
|
+
legacy: false,
|
|
44
|
+
add_original_image: true,
|
|
45
|
+
cfg_rescale: 0,
|
|
46
|
+
noise_schedule: "karras",
|
|
47
|
+
legacy_v3_extend: false,
|
|
48
|
+
skip_cfg_above_sigma: 58,
|
|
49
|
+
use_coords: true,
|
|
50
|
+
normalize_reference_strength_multiple: true,
|
|
51
|
+
inpaintImg2ImgStrength: 1,
|
|
52
|
+
v4_prompt: {
|
|
53
|
+
caption: {
|
|
54
|
+
base_caption: enhancedPrompt,
|
|
55
|
+
char_captions: []
|
|
56
|
+
},
|
|
57
|
+
use_coords: true,
|
|
58
|
+
use_order: true
|
|
59
|
+
},
|
|
60
|
+
v4_negative_prompt: {
|
|
61
|
+
caption: {
|
|
62
|
+
base_caption: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry",
|
|
63
|
+
char_captions: []
|
|
64
|
+
},
|
|
65
|
+
legacy_uc: false
|
|
66
|
+
},
|
|
67
|
+
uc: "",
|
|
68
|
+
seed: Math.floor(Math.random() * 4294967295),
|
|
69
|
+
legacy_uc: false,
|
|
70
|
+
characterPrompts: [],
|
|
71
|
+
deliberate_euler_ancestral_bug: false,
|
|
72
|
+
prefer_brownian: true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const response = await fetch(NOVELAI_API_URL, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'Authorization': `Bearer ${NOVELAI_API_TOKEN}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'Accept': 'application/x-zip-compressed',
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(payload),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`NovelAI API error: ${response.statusText}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const zipBuffer = await response.arrayBuffer()
|
|
91
|
+
const zip = await JSZip.loadAsync(zipBuffer)
|
|
92
|
+
|
|
93
|
+
const imageFile = Object.values(zip.files)[0]
|
|
94
|
+
if (!imageFile) {
|
|
95
|
+
throw new Error('No image found in response')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// get binary buffer (node buffer / Uint8Array)
|
|
99
|
+
const bufferData = await imageFile.async('nodebuffer') as Uint8Array
|
|
100
|
+
const buffer = Buffer.from(bufferData)
|
|
101
|
+
|
|
102
|
+
// Use filename from zip entry if available, otherwise generate one
|
|
103
|
+
const filenameBase = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.png`
|
|
104
|
+
const filename = `novelai/${filenameBase}`
|
|
105
|
+
|
|
106
|
+
// Upload to configured bucket and return public URL
|
|
107
|
+
const publicUrl = await uploadBuffer(buffer, filename, 'image/png')
|
|
108
|
+
return publicUrl
|
|
109
|
+
|
|
110
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
VStack,
|
|
8
|
+
HStack,
|
|
9
|
+
Heading,
|
|
10
|
+
Text,
|
|
11
|
+
Button,
|
|
12
|
+
Badge,
|
|
13
|
+
Icon,
|
|
14
|
+
Box,
|
|
15
|
+
Code,
|
|
16
|
+
Collapsible,
|
|
17
|
+
IconButton
|
|
18
|
+
} from '@chakra-ui/react'
|
|
19
|
+
import {
|
|
20
|
+
FaGoogle,
|
|
21
|
+
FaFacebook,
|
|
22
|
+
FaInstagram,
|
|
23
|
+
FaChartLine,
|
|
24
|
+
FaLink,
|
|
25
|
+
FaUnlink,
|
|
26
|
+
FaSync,
|
|
27
|
+
FaCopy,
|
|
28
|
+
FaChevronDown,
|
|
29
|
+
FaChevronUp
|
|
30
|
+
} from 'react-icons/fa'
|
|
31
|
+
import { IconType } from 'react-icons'
|
|
32
|
+
import { toaster } from '@/components/ui/toaster'
|
|
33
|
+
import { clientTokenStorage } from '@/lib/client-token-storage'
|
|
34
|
+
|
|
35
|
+
const iconMap: Record<string, IconType> = {
|
|
36
|
+
FaGoogle,
|
|
37
|
+
FaFacebook,
|
|
38
|
+
FaInstagram,
|
|
39
|
+
FaChartLine
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Platform {
|
|
43
|
+
id: string
|
|
44
|
+
name: string
|
|
45
|
+
icon: string
|
|
46
|
+
description: string
|
|
47
|
+
scopes: string[]
|
|
48
|
+
color: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface OAuthCardProps {
|
|
52
|
+
platform: Platform
|
|
53
|
+
userEmail?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function OAuthCard({ platform, userEmail }: OAuthCardProps) {
|
|
57
|
+
const [isConnected, setIsConnected] = useState(false)
|
|
58
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
59
|
+
const [token, setToken] = useState<string | null>(null)
|
|
60
|
+
const [showToken, setShowToken] = useState(false)
|
|
61
|
+
const searchParams = useSearchParams()
|
|
62
|
+
|
|
63
|
+
const IconComponent = iconMap[platform.icon] || FaLink
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const success = searchParams.get('success')
|
|
67
|
+
const error = searchParams.get('error')
|
|
68
|
+
const connectedPlatform = searchParams.get('platform')
|
|
69
|
+
const encodedToken = searchParams.get('token')
|
|
70
|
+
|
|
71
|
+
if (success === 'true' && connectedPlatform === platform.id) {
|
|
72
|
+
setIsConnected(true)
|
|
73
|
+
|
|
74
|
+
// Store token in IndexedDB if provided
|
|
75
|
+
if (encodedToken) {
|
|
76
|
+
try {
|
|
77
|
+
const tokenData = JSON.parse(Buffer.from(encodedToken, 'base64').toString())
|
|
78
|
+
clientTokenStorage.storeToken({
|
|
79
|
+
id: platform.id,
|
|
80
|
+
platform: platform.name,
|
|
81
|
+
accessToken: tokenData.accessToken,
|
|
82
|
+
expiresAt: tokenData.expiresAt,
|
|
83
|
+
lastRefreshed: new Date().toISOString()
|
|
84
|
+
})
|
|
85
|
+
setToken(tokenData.accessToken)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error('Failed to store token:', err)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
toaster.create({
|
|
92
|
+
title: 'Connected successfully',
|
|
93
|
+
description: `${platform.name} has been connected`,
|
|
94
|
+
type: 'success',
|
|
95
|
+
duration: 5000,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (error && connectedPlatform === platform.id) {
|
|
100
|
+
toaster.create({
|
|
101
|
+
title: 'Connection failed',
|
|
102
|
+
description: `Failed to connect ${platform.name}: ${error}`,
|
|
103
|
+
type: 'error',
|
|
104
|
+
duration: 5000,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}, [searchParams, platform])
|
|
108
|
+
|
|
109
|
+
// Load token from IndexedDB on mount
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const loadToken = async () => {
|
|
112
|
+
try {
|
|
113
|
+
const storedToken = await clientTokenStorage.getToken(platform.id)
|
|
114
|
+
if (storedToken) {
|
|
115
|
+
setToken(storedToken.accessToken)
|
|
116
|
+
setIsConnected(true)
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('Failed to load token:', err)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
loadToken()
|
|
123
|
+
}, [platform.id])
|
|
124
|
+
|
|
125
|
+
const handleConnect = async () => {
|
|
126
|
+
setIsLoading(true)
|
|
127
|
+
try {
|
|
128
|
+
const params = new URLSearchParams({
|
|
129
|
+
platform: platform.id,
|
|
130
|
+
scopes: platform.scopes.join(','),
|
|
131
|
+
userEmail: userEmail || ''
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
window.location.href = `/api/oauth/connect?${params.toString()}`
|
|
135
|
+
} catch (error) {
|
|
136
|
+
toaster.create({
|
|
137
|
+
title: 'Connection failed',
|
|
138
|
+
description: 'Unable to initiate OAuth connection',
|
|
139
|
+
type: 'error',
|
|
140
|
+
duration: 5000,
|
|
141
|
+
})
|
|
142
|
+
setIsLoading(false)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handleDisconnect = async () => {
|
|
147
|
+
setIsLoading(true)
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(`/api/oauth/disconnect`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ platform: platform.id })
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (!response.ok) throw new Error('Disconnection failed')
|
|
156
|
+
|
|
157
|
+
setIsConnected(false)
|
|
158
|
+
setToken(null)
|
|
159
|
+
|
|
160
|
+
// Remove token from IndexedDB
|
|
161
|
+
await clientTokenStorage.removeToken(platform.id)
|
|
162
|
+
|
|
163
|
+
toaster.create({
|
|
164
|
+
title: 'Disconnected',
|
|
165
|
+
description: `${platform.name} has been disconnected`,
|
|
166
|
+
type: 'success',
|
|
167
|
+
duration: 3000,
|
|
168
|
+
})
|
|
169
|
+
} catch (error) {
|
|
170
|
+
toaster.create({
|
|
171
|
+
title: 'Disconnection failed',
|
|
172
|
+
description: 'Unable to disconnect platform',
|
|
173
|
+
type: 'error',
|
|
174
|
+
duration: 5000,
|
|
175
|
+
})
|
|
176
|
+
} finally {
|
|
177
|
+
setIsLoading(false)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const handleRefreshToken = async () => {
|
|
182
|
+
setIsLoading(true)
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`/api/oauth/refresh`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
187
|
+
body: JSON.stringify({ platform: platform.id })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if (!response.ok) throw new Error('Token refresh failed')
|
|
191
|
+
|
|
192
|
+
const data = await response.json()
|
|
193
|
+
setToken(data.accessToken)
|
|
194
|
+
|
|
195
|
+
// Update token in IndexedDB
|
|
196
|
+
await clientTokenStorage.storeToken({
|
|
197
|
+
id: platform.id,
|
|
198
|
+
platform: platform.name,
|
|
199
|
+
accessToken: data.accessToken,
|
|
200
|
+
expiresAt: data.expiresIn ?
|
|
201
|
+
new Date(Date.now() + data.expiresIn * 1000).toISOString() :
|
|
202
|
+
undefined,
|
|
203
|
+
lastRefreshed: new Date().toISOString()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
toaster.create({
|
|
207
|
+
title: 'Token refreshed',
|
|
208
|
+
description: 'Your access token has been refreshed',
|
|
209
|
+
type: 'success',
|
|
210
|
+
duration: 3000,
|
|
211
|
+
})
|
|
212
|
+
} catch (error) {
|
|
213
|
+
toaster.create({
|
|
214
|
+
title: 'Refresh failed',
|
|
215
|
+
description: 'Unable to refresh token',
|
|
216
|
+
type: 'error',
|
|
217
|
+
duration: 5000,
|
|
218
|
+
})
|
|
219
|
+
} finally {
|
|
220
|
+
setIsLoading(false)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const copyToken = () => {
|
|
225
|
+
if (token) {
|
|
226
|
+
navigator.clipboard.writeText(token)
|
|
227
|
+
toaster.create({
|
|
228
|
+
title: 'Copied!',
|
|
229
|
+
description: 'Token copied to clipboard',
|
|
230
|
+
type: 'success',
|
|
231
|
+
duration: 2000,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<Card.Root variant="outline" size="lg">
|
|
238
|
+
<Card.Header>
|
|
239
|
+
<HStack justify="space-between">
|
|
240
|
+
<HStack gap={3}>
|
|
241
|
+
<Icon as={IconComponent} boxSize={6} color={platform.color} />
|
|
242
|
+
<Heading size="md">{platform.name}</Heading>
|
|
243
|
+
</HStack>
|
|
244
|
+
<Badge colorScheme={isConnected ? 'green' : 'gray'}>
|
|
245
|
+
{isConnected ? 'Connected' : 'Not Connected'}
|
|
246
|
+
</Badge>
|
|
247
|
+
</HStack>
|
|
248
|
+
</Card.Header>
|
|
249
|
+
|
|
250
|
+
<Card.Body>
|
|
251
|
+
<VStack align="stretch" gap={4}>
|
|
252
|
+
<Text color="fg.muted" fontSize="sm">
|
|
253
|
+
{platform.description}
|
|
254
|
+
</Text>
|
|
255
|
+
|
|
256
|
+
<Box>
|
|
257
|
+
<Text fontSize="sm" fontWeight="medium" mb={1}>Scopes:</Text>
|
|
258
|
+
<Text fontSize="xs" color="fg.muted" lineClamp={2}>
|
|
259
|
+
{platform.scopes.join(', ')}
|
|
260
|
+
</Text>
|
|
261
|
+
</Box>
|
|
262
|
+
|
|
263
|
+
<HStack gap={2}>
|
|
264
|
+
{!isConnected ? (
|
|
265
|
+
<Button
|
|
266
|
+
colorScheme="blue"
|
|
267
|
+
onClick={handleConnect}
|
|
268
|
+
loading={isLoading}
|
|
269
|
+
loadingText="Connecting..."
|
|
270
|
+
size="sm"
|
|
271
|
+
width="full"
|
|
272
|
+
>
|
|
273
|
+
<FaLink />
|
|
274
|
+
Connect
|
|
275
|
+
</Button>
|
|
276
|
+
) : (
|
|
277
|
+
<>
|
|
278
|
+
<Button
|
|
279
|
+
variant="outline"
|
|
280
|
+
onClick={handleRefreshToken}
|
|
281
|
+
loading={isLoading}
|
|
282
|
+
size="sm"
|
|
283
|
+
flex={1}
|
|
284
|
+
>
|
|
285
|
+
<FaSync />
|
|
286
|
+
Refresh
|
|
287
|
+
</Button>
|
|
288
|
+
<Button
|
|
289
|
+
variant="outline"
|
|
290
|
+
colorScheme="red"
|
|
291
|
+
onClick={handleDisconnect}
|
|
292
|
+
loading={isLoading}
|
|
293
|
+
size="sm"
|
|
294
|
+
flex={1}
|
|
295
|
+
>
|
|
296
|
+
<FaUnlink />
|
|
297
|
+
Disconnect
|
|
298
|
+
</Button>
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
</HStack>
|
|
302
|
+
|
|
303
|
+
{isConnected && token && (
|
|
304
|
+
<Box>
|
|
305
|
+
<HStack justify="space-between" mb={2}>
|
|
306
|
+
<Text fontSize="sm" fontWeight="medium">Access Token:</Text>
|
|
307
|
+
<HStack gap={1}>
|
|
308
|
+
<IconButton
|
|
309
|
+
aria-label="Copy token"
|
|
310
|
+
size="xs"
|
|
311
|
+
variant="ghost"
|
|
312
|
+
onClick={copyToken}
|
|
313
|
+
>
|
|
314
|
+
<FaCopy />
|
|
315
|
+
</IconButton>
|
|
316
|
+
<IconButton
|
|
317
|
+
aria-label="Toggle token visibility"
|
|
318
|
+
size="xs"
|
|
319
|
+
variant="ghost"
|
|
320
|
+
onClick={() => setShowToken(!showToken)}
|
|
321
|
+
>
|
|
322
|
+
{showToken ? <FaChevronUp /> : <FaChevronDown />}
|
|
323
|
+
</IconButton>
|
|
324
|
+
</HStack>
|
|
325
|
+
</HStack>
|
|
326
|
+
<Collapsible.Root open={showToken}>
|
|
327
|
+
<Collapsible.Content>
|
|
328
|
+
<Code
|
|
329
|
+
p={3}
|
|
330
|
+
borderRadius="md"
|
|
331
|
+
fontSize="xs"
|
|
332
|
+
display="block"
|
|
333
|
+
overflowX="auto"
|
|
334
|
+
whiteSpace="pre"
|
|
335
|
+
>
|
|
336
|
+
{token}
|
|
337
|
+
</Code>
|
|
338
|
+
</Collapsible.Content>
|
|
339
|
+
</Collapsible.Root>
|
|
340
|
+
</Box>
|
|
341
|
+
)}
|
|
342
|
+
</VStack>
|
|
343
|
+
</Card.Body>
|
|
344
|
+
</Card.Root>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react'
|
|
4
|
+
|
|
5
|
+
export const AirtableIcon = forwardRef<SVGSVGElement>((props, ref) => (
|
|
6
|
+
<svg ref={ref} viewBox='0 0 20 16' xmlns='http://www.w3.org/2000/svg' {...props}><path d='m8.50297526.20100591-7.09941044 2.93764625c-.39480037.16338794-.39071029.72415946.00656565.88162754l7.12911731 2.82710645c.62642805.24841855 1.32400201.24841855 1.95032242 0l7.129225-2.82710645c.3971683-.15746808.401366-.7182396.006458-.88162754l-7.0993028-2.93764625c-.6477395-.26800788-1.37534322-.26800788-2.02297514 0' fill='#ffbf00'/><path d='m10.1469076 8.39120841v7.06249209c0 .3359247.3387232.565938.6509686.4421592l7.9440119-3.0834899c.1813628-.0718993.300298-.2471269.300298-.4421592v-7.06249207c0-.33592474-.3387232-.56593793-.6509685-.44215918l-7.944012 3.08348988c-.1812551.07189931-.300298.24712699-.300298.44215918' fill='#26b5f8'/><path d='m8.29192714 8.75561302-2.35760833 1.13833392-.23937732.11570616-4.97676647 2.3846244c-.31547434.152194-.71813197-.0777115-.71813197-.4281668v-6.62872832c0-.12679249.06501075-.23625595.15219404-.31870335.03638019-.03648782.07760389-.06651762.1204421-.09030467.11893522-.07136113.28856591-.09041229.43279506-.03336644l7.54684367 2.99017147c.38360647.15219408.41374392.68971668.03960922.87043363' fill='#ed3049'/><path d='m8.29192714 8.75561302-2.35760833 1.13833392-5.78208172-4.87526791c.03638019-.03648782.07760389-.06651762.1204421-.09030467.11893522-.07136113.28856591-.09041229.43279506-.03336644l7.54684367 2.99017147c.38360647.15219408.41374392.68971668.03960922.87043363' fillOpacity='.25'/></svg>
|
|
7
|
+
))
|
|
8
|
+
|
|
9
|
+
export const LinkedInIcon = forwardRef<SVGSVGElement>((props, ref) => (
|
|
10
|
+
<svg ref={ref} height='72' viewBox='0 0 72 72' width='72' xmlns='http://www.w3.org/2000/svg' {...props}><g fill='none' fillRule='evenodd'><path d='M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z' fill='#007EBB'/><path d='M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z' fill='#FFF'/></g></svg>
|
|
11
|
+
))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import ReactMarkdown from 'react-markdown'
|
|
3
|
+
import gfm from 'remark-gfm'
|
|
4
|
+
import breaks from 'remark-breaks'
|
|
5
|
+
import { Link, Text } from '@chakra-ui/react'
|
|
6
|
+
|
|
7
|
+
export default function Markdown({ children } : {children: string}) {
|
|
8
|
+
return (
|
|
9
|
+
<ReactMarkdown
|
|
10
|
+
components={{
|
|
11
|
+
a: props => <Link target='_blank' {...props}/>,
|
|
12
|
+
// @ts-expect-error
|
|
13
|
+
em: props => <Text as='i' fontStyle='italic' {...props} />,
|
|
14
|
+
}}
|
|
15
|
+
remarkPlugins={[breaks, gfm]}
|
|
16
|
+
>{children}</ReactMarkdown>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { forwardRef } from 'react'
|
|
3
|
+
import { List, Stack, HStack, VStack } from '@chakra-ui/react'
|
|
4
|
+
import useForwardAs from '@/hooks/useForwardAs'
|
|
5
|
+
|
|
6
|
+
export type StackListProps = List.RootProps
|
|
7
|
+
|
|
8
|
+
export const StackList = forwardRef<typeof Stack, StackListProps>(({ as, ...props }, ref) => (
|
|
9
|
+
<List.Root
|
|
10
|
+
// @ts-expect-error
|
|
11
|
+
ref={ref}
|
|
12
|
+
as={useForwardAs(Stack, as)} {...props} />
|
|
13
|
+
))
|
|
14
|
+
|
|
15
|
+
export const HList = forwardRef<typeof HStack, StackListProps>(({ as, ...props }, ref) => (
|
|
16
|
+
<StackList ref={ref} as={useForwardAs(HStack, as)} {...props} />
|
|
17
|
+
))
|
|
18
|
+
|
|
19
|
+
export const VList = forwardRef<typeof HStack, StackListProps>(({ as, ...props }, ref) => (
|
|
20
|
+
<StackList ref={ref} as={useForwardAs(VStack, as)} {...props} />
|
|
21
|
+
))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { ElementType, forwardRef } from 'react'
|
|
3
|
+
import { StackList, VList, HList, StackListProps } from '@/components/stack/list'
|
|
4
|
+
import useForwardAs from '@/hooks/useForwardAs'
|
|
5
|
+
|
|
6
|
+
export type StackMenuProps = StackListProps
|
|
7
|
+
|
|
8
|
+
export const StackMenu = forwardRef<typeof StackList, StackMenuProps>(({ children, as, ...props }, ref) => (
|
|
9
|
+
<StackList
|
|
10
|
+
// @ts-expect-error
|
|
11
|
+
ref={ref}
|
|
12
|
+
as={as ? useForwardAs(as, 'menu') : 'menu'}
|
|
13
|
+
// List Defaults
|
|
14
|
+
p={0} m={0} listStyleType='none'
|
|
15
|
+
// Alignment
|
|
16
|
+
justifyContent='space-between'
|
|
17
|
+
{...props}>{children}</StackList>
|
|
18
|
+
))
|
|
19
|
+
|
|
20
|
+
export const HMenu = forwardRef<typeof HList, StackMenuProps>(({ children, ...props }, ref) => (
|
|
21
|
+
<StackMenu ref={ref} as={HList} {...props}>{children}</StackMenu>
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
export const VMenu = forwardRef<typeof VList, StackMenuProps>(({ children, ...props }, ref) => (
|
|
25
|
+
<StackMenu ref={ref} as={VList} {...props}>{children}</StackMenu>
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
// Menu Item, keep it here in this file
|
|
29
|
+
import { Button, ButtonProps, ListItem } from '@chakra-ui/react'
|
|
30
|
+
|
|
31
|
+
export type MenuItemProps = ButtonProps
|
|
32
|
+
export const MenuItem = forwardRef<ElementType, MenuItemProps>(({ children, ...props }, ref) => (
|
|
33
|
+
<ListItem
|
|
34
|
+
// @ts-expect-error
|
|
35
|
+
ref={ref}>
|
|
36
|
+
<Button size='sm' variant='ghost' {...props}>
|
|
37
|
+
{children}
|
|
38
|
+
</Button>
|
|
39
|
+
</ListItem>
|
|
40
|
+
))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { forwardRef } from 'react'
|
|
3
|
+
import { StackList, VList, HList, StackListProps } from '@/components/stack/list'
|
|
4
|
+
import useForwardAs from '@/hooks/useForwardAs'
|
|
5
|
+
|
|
6
|
+
export type StackNavProps = StackListProps
|
|
7
|
+
|
|
8
|
+
export const StackNav = forwardRef<typeof StackList, StackListProps>(({ children, as, ...props }, ref) => (
|
|
9
|
+
<StackList
|
|
10
|
+
// @ts-expect-error
|
|
11
|
+
ref={ref}
|
|
12
|
+
as={as ? useForwardAs(as, 'nav') : 'nav'}
|
|
13
|
+
// List Defaults
|
|
14
|
+
p={0} m={0} listStyleType='none'
|
|
15
|
+
// Alignment
|
|
16
|
+
justifyContent='space-between'
|
|
17
|
+
{...props}>{children}</StackList>
|
|
18
|
+
))
|
|
19
|
+
|
|
20
|
+
export const HNav = forwardRef<typeof HList, StackListProps>(({ children, ...props }, ref) => (
|
|
21
|
+
<StackNav ref={ref} as={HList} {...props}>{children}</StackNav>
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
export const VNav = forwardRef<typeof VList, StackListProps>(({ children, ...props }, ref) => (
|
|
25
|
+
<StackNav ref={ref} as={VList} {...props}>{children}</StackNav>
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
import { ListItem, ListItemProps, Link, LinkProps } from '@chakra-ui/react'
|
|
29
|
+
|
|
30
|
+
export type NavItemProps = LinkProps
|
|
31
|
+
|
|
32
|
+
export const NavItem = forwardRef<typeof Link, NavItemProps>(({ children, as, ...props }, ref) => (
|
|
33
|
+
<ListItem
|
|
34
|
+
// @ts-expect-error
|
|
35
|
+
ref={ref}
|
|
36
|
+
{...props}>
|
|
37
|
+
<Link {...props}>{children}</Link>
|
|
38
|
+
</ListItem>
|
|
39
|
+
))
|