Skip to content

Commit 00f3f8f

Browse files
chore: update artifacts [skip ci]
1 parent 3a3afb2 commit 00f3f8f

100 files changed

Lines changed: 5409 additions & 1864 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
249 KB
Loading

public/ai-chatgpt/screenshot.png

249 KB
Loading
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type {PageContext} from './content/scripts'
2+
3+
const isFirefoxLike =
4+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
5+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
6+
7+
if (isFirefoxLike) {
8+
browser.browserAction.onClicked.addListener(() => {
9+
browser.sidebarAction.open()
10+
})
11+
} else {
12+
chrome.action.onClicked.addListener(() => {
13+
chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
14+
})
15+
}
16+
17+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
18+
if (message?.type === 'openSidebar') {
19+
if (isFirefoxLike) {
20+
browser.sidebarAction.open()
21+
return
22+
}
23+
// Must be invoked synchronously inside the message handler so the
24+
// user-gesture context from the content-script click is preserved.
25+
chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
26+
const tabId = sender.tab?.id
27+
if (chrome.sidePanel.open && tabId !== undefined) {
28+
try {
29+
chrome.sidePanel.open({tabId})
30+
} catch (error) {
31+
console.error(error)
32+
}
33+
}
34+
return
35+
}
36+
37+
if (message?.type !== 'getActiveTabContext') return
38+
39+
;(async () => {
40+
try {
41+
const [tab] = await chrome.tabs.query({
42+
active: true,
43+
lastFocusedWindow: true
44+
})
45+
if (!tab?.id) {
46+
sendResponse({ok: false, error: 'No active tab'})
47+
return
48+
}
49+
const context = (await chrome.tabs.sendMessage(tab.id, {
50+
type: 'getPageContext'
51+
})) as PageContext | undefined
52+
if (!context) {
53+
sendResponse({ok: false, error: 'No page context received'})
54+
return
55+
}
56+
sendResponse({ok: true, context})
57+
} catch (err) {
58+
const error = err instanceof Error ? err.message : String(err)
59+
sendResponse({ok: false, error})
60+
}
61+
})()
62+
63+
return true
64+
})
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {useState} from 'react'
2+
import {KeyRound} from 'lucide-react'
3+
import {Button} from './ui/button'
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardHeader,
9+
CardTitle
10+
} from './ui/card'
11+
12+
interface ApiKeyFormProps {
13+
onSubmit: (key: string) => void
14+
}
15+
16+
export default function ApiKeyForm({onSubmit}: ApiKeyFormProps) {
17+
const [key, setKey] = useState('')
18+
19+
function handleSubmit(e: React.FormEvent) {
20+
e.preventDefault()
21+
const trimmed = key.trim()
22+
if (trimmed) {
23+
onSubmit(trimmed)
24+
}
25+
}
26+
27+
return (
28+
<div className="flex h-screen items-center justify-center p-4">
29+
<Card className="w-full">
30+
<CardHeader>
31+
<div className="flex items-center gap-2">
32+
<KeyRound className="size-5 text-muted-foreground" />
33+
<CardTitle>ChatGPT API Key</CardTitle>
34+
</div>
35+
<CardDescription>
36+
Enter your ChatGPT API key to start chatting. Your key is stored
37+
locally in extension storage and never sent anywhere except
38+
ChatGPT's API.
39+
</CardDescription>
40+
</CardHeader>
41+
<CardContent>
42+
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
43+
<input
44+
type="password"
45+
value={key}
46+
onChange={(e) => setKey(e.target.value)}
47+
placeholder="sk-..."
48+
autoFocus
49+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
50+
/>
51+
<Button type="submit" disabled={!key.trim()}>
52+
Save & Start Chatting
53+
</Button>
54+
<p className="text-xs text-muted-foreground">
55+
Get your API key at{' '}
56+
<a
57+
href="https://platform.openai.com/api-keys"
58+
target="_blank"
59+
rel="noopener noreferrer"
60+
className="underline"
61+
>
62+
platform.openai.com
63+
</a>
64+
</p>
65+
</form>
66+
</CardContent>
67+
</Card>
68+
</div>
69+
)
70+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {useState, useRef, useEffect} from 'react'
2+
import {SendHorizontal} from 'lucide-react'
3+
import {Button} from './ui/button'
4+
5+
interface ChatInputProps {
6+
onSend: (message: string) => void
7+
disabled?: boolean
8+
}
9+
10+
export default function ChatInput({onSend, disabled}: ChatInputProps) {
11+
const [input, setInput] = useState('')
12+
const textareaRef = useRef<HTMLTextAreaElement>(null)
13+
14+
useEffect(() => {
15+
if (textareaRef.current) {
16+
textareaRef.current.style.height = 'auto'
17+
textareaRef.current.style.height =
18+
Math.min(textareaRef.current.scrollHeight, 120) + 'px'
19+
}
20+
}, [input])
21+
22+
function handleSubmit(e: React.FormEvent) {
23+
e.preventDefault()
24+
const trimmed = input.trim()
25+
if (trimmed && !disabled) {
26+
onSend(trimmed)
27+
setInput('')
28+
}
29+
}
30+
31+
function handleKeyDown(e: React.KeyboardEvent) {
32+
if (e.key === 'Enter' && !e.shiftKey) {
33+
e.preventDefault()
34+
handleSubmit(e)
35+
}
36+
}
37+
38+
return (
39+
<form onSubmit={handleSubmit} className="flex items-end gap-2 border-t p-3">
40+
<textarea
41+
ref={textareaRef}
42+
value={input}
43+
onChange={(e) => setInput(e.target.value)}
44+
onKeyDown={handleKeyDown}
45+
placeholder="Message ChatGPT..."
46+
disabled={disabled}
47+
rows={1}
48+
className="flex min-h-9 max-h-[120px] flex-1 resize-none rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
49+
/>
50+
<Button
51+
type="submit"
52+
size="icon"
53+
disabled={disabled || !input.trim()}
54+
aria-label="Send message"
55+
>
56+
<SendHorizontal className="size-4" />
57+
</Button>
58+
</form>
59+
)
60+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {Bot, User} from 'lucide-react'
2+
import ReactMarkdown from 'react-markdown'
3+
import type {Message} from '../lib/client'
4+
import {cn} from '../lib/utils'
5+
6+
interface ChatMessageProps {
7+
message: Message
8+
}
9+
10+
export default function ChatMessage({message}: ChatMessageProps) {
11+
const isUser = message.role === 'user'
12+
13+
return (
14+
<div
15+
className={cn(
16+
'flex gap-3 px-4 py-3',
17+
isUser ? 'bg-transparent' : 'bg-muted/50'
18+
)}
19+
>
20+
<div
21+
className={cn(
22+
'flex size-7 shrink-0 items-center justify-center rounded-md',
23+
isUser
24+
? 'bg-primary text-primary-foreground'
25+
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300'
26+
)}
27+
>
28+
{isUser ? <User className="size-4" /> : <Bot className="size-4" />}
29+
</div>
30+
<div className="min-w-0 flex-1 text-sm leading-relaxed">
31+
{isUser ? (
32+
<p className="whitespace-pre-wrap">{message.content}</p>
33+
) : (
34+
<div className="prose prose-sm dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
35+
<ReactMarkdown>{message.content}</ReactMarkdown>
36+
</div>
37+
)}
38+
</div>
39+
</div>
40+
)
41+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from 'react'
2+
import {Slot} from '@radix-ui/react-slot'
3+
import {cva, type VariantProps} from 'class-variance-authority'
4+
5+
import {cn} from '@/lib/utils'
6+
7+
const buttonVariants = cva(
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9+
{
10+
variants: {
11+
variant: {
12+
default:
13+
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
14+
destructive:
15+
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
16+
outline:
17+
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
18+
secondary:
19+
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
20+
ghost:
21+
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
22+
link: 'text-primary underline-offset-4 hover:underline'
23+
},
24+
size: {
25+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
26+
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
27+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
28+
icon: 'size-9'
29+
}
30+
},
31+
defaultVariants: {
32+
variant: 'default',
33+
size: 'default'
34+
}
35+
}
36+
)
37+
38+
function Button({
39+
className,
40+
variant,
41+
size,
42+
asChild = false,
43+
...props
44+
}: React.ComponentProps<'button'> &
45+
VariantProps<typeof buttonVariants> & {
46+
asChild?: boolean
47+
}) {
48+
const Comp = asChild ? Slot : 'button'
49+
50+
return (
51+
<Comp
52+
data-slot="button"
53+
className={cn(buttonVariants({variant, size, className}))}
54+
{...props}
55+
/>
56+
)
57+
}
58+
59+
export {Button, buttonVariants}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as React from 'react'
2+
3+
import {cn} from '../../lib/utils'
4+
5+
function Card({
6+
className,
7+
...props
8+
}: React.ComponentProps<'div'>): React.JSX.Element {
9+
return (
10+
<div
11+
data-slot="card"
12+
className={cn(
13+
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
14+
className
15+
)}
16+
{...props}
17+
/>
18+
)
19+
}
20+
21+
function CardHeader({
22+
className,
23+
...props
24+
}: React.ComponentProps<'div'>): React.JSX.Element {
25+
return (
26+
<div
27+
data-slot="card-header"
28+
className={cn(
29+
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
30+
className
31+
)}
32+
{...props}
33+
/>
34+
)
35+
}
36+
37+
function CardTitle({
38+
className,
39+
...props
40+
}: React.ComponentProps<'div'>): React.JSX.Element {
41+
return (
42+
<div
43+
data-slot="card-title"
44+
className={cn('leading-none font-semibold', className)}
45+
{...props}
46+
/>
47+
)
48+
}
49+
50+
function CardDescription({
51+
className,
52+
...props
53+
}: React.ComponentProps<'div'>): React.JSX.Element {
54+
return (
55+
<div
56+
data-slot="card-description"
57+
className={cn('text-muted-foreground text-sm', className)}
58+
{...props}
59+
/>
60+
)
61+
}
62+
63+
function CardContent({
64+
className,
65+
...props
66+
}: React.ComponentProps<'div'>): React.JSX.Element {
67+
return (
68+
<div
69+
data-slot="card-content"
70+
className={cn('px-6', className)}
71+
{...props}
72+
/>
73+
)
74+
}
75+
76+
function CardFooter({
77+
className,
78+
...props
79+
}: React.ComponentProps<'div'>): React.JSX.Element {
80+
return (
81+
<div
82+
data-slot="card-footer"
83+
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
84+
{...props}
85+
/>
86+
)
87+
}
88+
89+
export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent}

0 commit comments

Comments
 (0)