11"use client" ;
22
3- import { useState , useEffect , useRef } from "react" ;
3+ import { useEffect , useRef } from "react" ;
44import Link from "next/link" ;
5- import {
6- Server ,
7- Shield ,
8- Users ,
9- Zap ,
10- Eye ,
11- Code ,
12- Brain ,
13- Copy ,
14- Check ,
15- Download ,
16- X ,
17- } from "lucide-react" ;
5+ import { Server , Shield , Users , Zap , Eye , Code , Brain } from "lucide-react" ;
186import { StoryEmbed } from "@/components/story-embed" ;
7+ import { QuickStartSelector } from "@/components/quick-start-selector" ;
198
209function GitHubIcon ( { className } : { className ?: string } ) {
2110 return (
@@ -25,307 +14,6 @@ function GitHubIcon({ className }: { className?: string }) {
2514 ) ;
2615}
2716
28- // --- Quick Start Selector ---
29-
30- type Method = "binary" | "docker" | "cargo" ;
31- type OS = "linux-x86_64" | "linux-arm64" | "macos-arm64" | "windows" ;
32- type Profile = "full" | "standard" | "minimal" | "tiny" ;
33- type Libc = "gnu" | "musl" ;
34-
35- const osLabels : Record < OS , string > = {
36- "linux-x86_64" : "Linux x86_64" ,
37- "linux-arm64" : "Linux ARM64" ,
38- "macos-arm64" : "macOS ARM64" ,
39- windows : "Windows" ,
40- } ;
41-
42- const libcLabels : Record < Libc , string > = {
43- gnu : "glibc" ,
44- musl : "musl" ,
45- } ;
46-
47- function getTarget ( os : OS , libc : Libc ) : string {
48- switch ( os ) {
49- case "linux-x86_64" :
50- return libc === "musl" ? "x86_64-unknown-linux-musl" : "x86_64-unknown-linux-gnu" ;
51- case "linux-arm64" :
52- return "aarch64-unknown-linux-gnu" ;
53- case "macos-arm64" :
54- return "aarch64-apple-darwin" ;
55- case "windows" :
56- return "x86_64-pc-windows-msvc" ;
57- }
58- }
59-
60- const profileSummaries : Record < Profile , string > = {
61- full : "Everything" ,
62- standard : "Production deployment" ,
63- minimal : "Development and embedded use" ,
64- tiny : "Stateless proxy" ,
65- } ;
66-
67- const featureMatrix : { name : string ; profiles : Profile [ ] } [ ] = [
68- { name : "OpenAI" , profiles : [ "tiny" , "minimal" , "standard" , "full" ] } ,
69- { name : "Anthropic" , profiles : [ "minimal" , "standard" , "full" ] } ,
70- { name : "AWS Bedrock" , profiles : [ "minimal" , "standard" , "full" ] } ,
71- { name : "Google Vertex AI" , profiles : [ "minimal" , "standard" , "full" ] } ,
72- { name : "Azure OpenAI" , profiles : [ "minimal" , "standard" , "full" ] } ,
73- { name : "SQLite" , profiles : [ "minimal" , "standard" , "full" ] } ,
74- { name : "Embedded UI" , profiles : [ "minimal" , "standard" , "full" ] } ,
75- { name : "Setup wizard" , profiles : [ "minimal" , "standard" , "full" ] } ,
76- { name : "PostgreSQL" , profiles : [ "standard" , "full" ] } ,
77- { name : "Redis caching" , profiles : [ "standard" , "full" ] } ,
78- { name : "SSO (OIDC / OAuth)" , profiles : [ "standard" , "full" ] } ,
79- { name : "CEL RBAC" , profiles : [ "standard" , "full" ] } ,
80- { name : "S3 storage" , profiles : [ "standard" , "full" ] } ,
81- { name : "Secrets managers" , profiles : [ "standard" , "full" ] } ,
82- { name : "OTLP & Prometheus" , profiles : [ "standard" , "full" ] } ,
83- { name : "OpenAPI docs" , profiles : [ "standard" , "full" ] } ,
84- { name : "Doc extraction" , profiles : [ "standard" , "full" ] } ,
85- { name : "SAML SSO" , profiles : [ "full" ] } ,
86- { name : "Kreuzberg OCR" , profiles : [ "full" ] } ,
87- { name : "ClamAV scanning" , profiles : [ "full" ] } ,
88- ] ;
89-
90- function getInstallCommand ( method : Method , os : OS , profile : Profile , libc : Libc ) : string {
91- if ( method === "docker" ) {
92- return [
93- "docker run \\" ,
94- " -p 8080:8080 \\" ,
95- " -e OPENROUTER_API_KEY=sk-... \\" ,
96- " ghcr.io/scriptsmith/hadrian" ,
97- ] . join ( "\n" ) ;
98- }
99- if ( method === "cargo" ) {
100- return "cargo install hadrian\nhadrian" ;
101- }
102- const ext = os === "windows" ? "zip" : "tar.gz" ;
103- const target = getTarget ( os , libc ) ;
104- const filename = `hadrian-${ target } -${ profile } .${ ext } ` ;
105- const url = `https://github.com/ScriptSmith/hadrian/releases/latest/download/${ filename } ` ;
106- if ( os === "windows" ) {
107- return [ `curl -LO \\` , ` ${ url } ` , `tar -xf ${ filename } ` , `.\\hadrian.exe` ] . join ( "\n" ) ;
108- }
109- return [ `curl -L \\` , ` ${ url } \\` , ` | tar xz` , `./hadrian` ] . join ( "\n" ) ;
110- }
111-
112- function getDownloadUrl ( os : OS , profile : Profile , libc : Libc ) : string {
113- const ext = os === "windows" ? "zip" : "tar.gz" ;
114- const target = getTarget ( os , libc ) ;
115- return `https://github.com/ScriptSmith/hadrian/releases/latest/download/hadrian-${ target } -${ profile } .${ ext } ` ;
116- }
117-
118- function ToggleGroup < T extends string > ( {
119- options,
120- value,
121- onChange,
122- labels,
123- disabled,
124- } : {
125- options : T [ ] ;
126- value : T ;
127- onChange : ( v : T ) => void ;
128- labels ?: Record < T , string > ;
129- disabled ?: Set < T > ;
130- } ) {
131- return (
132- < div className = "flex flex-wrap gap-1.5" >
133- { options . map ( ( opt ) => {
134- const isDisabled = disabled ?. has ( opt ) ;
135- return (
136- < button
137- key = { opt }
138- onClick = { ( ) => onChange ( opt ) }
139- disabled = { isDisabled }
140- className = { `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
141- isDisabled
142- ? "cursor-not-allowed bg-fd-muted text-fd-muted-foreground/40"
143- : value === opt
144- ? "bg-fd-primary text-fd-primary-foreground"
145- : "bg-fd-muted text-fd-muted-foreground hover:bg-fd-muted/80 hover:text-fd-foreground"
146- } `}
147- >
148- { labels ? labels [ opt ] : opt }
149- </ button >
150- ) ;
151- } ) }
152- </ div >
153- ) ;
154- }
155-
156- function getDisabledProfiles ( os : OS , libc : Libc ) : Set < Profile > | undefined {
157- if ( os === "windows" ) return new Set ( [ "full" , "standard" ] ) ;
158- if ( os === "linux-arm64" ) return new Set ( [ "full" ] ) ;
159- if ( os . startsWith ( "linux-" ) && libc === "musl" ) return new Set ( [ "full" ] ) ;
160- return undefined ;
161- }
162-
163- function QuickStartSelector ( ) {
164- const [ method , setMethod ] = useState < Method > ( "binary" ) ;
165- const [ os , setOs ] = useState < OS > ( "linux-x86_64" ) ;
166- const [ profile , setProfile ] = useState < Profile > ( "standard" ) ;
167- const [ libc , setLibc ] = useState < Libc > ( "musl" ) ;
168- const [ copied , setCopied ] = useState ( false ) ;
169-
170- const isLinux = os === "linux-x86_64" || os === "linux-arm64" ;
171- const disabledProfiles = getDisabledProfiles ( os , libc ) ;
172- const disabledLibcs = os === "linux-arm64" ? new Set < Libc > ( [ "musl" ] ) : undefined ;
173-
174- const handleOsChange = ( newOs : OS ) => {
175- setOs ( newOs ) ;
176- // Reset libc to gnu for non-Linux or ARM64 (no musl builds)
177- let newLibc = libc ;
178- if ( ! newOs . startsWith ( "linux-" ) || newOs === "linux-arm64" ) {
179- newLibc = "gnu" ;
180- setLibc ( "gnu" ) ;
181- }
182- // Adjust profile if it becomes unavailable
183- const disabled = getDisabledProfiles ( newOs , newLibc ) ;
184- if ( disabled ?. has ( profile ) ) {
185- setProfile ( disabled . has ( "standard" ) ? "minimal" : "standard" ) ;
186- }
187- } ;
188-
189- const handleLibcChange = ( newLibc : Libc ) => {
190- setLibc ( newLibc ) ;
191- if ( newLibc === "musl" && profile === "full" ) {
192- setProfile ( "standard" ) ;
193- }
194- } ;
195-
196- const command = getInstallCommand ( method , os , profile , libc ) ;
197- const downloadUrl = method === "binary" ? getDownloadUrl ( os , profile , libc ) : null ;
198-
199- const handleCopy = async ( ) => {
200- if ( navigator . clipboard ) {
201- await navigator . clipboard . writeText ( command ) ;
202- } else {
203- const textarea = document . createElement ( "textarea" ) ;
204- textarea . value = command ;
205- textarea . style . position = "fixed" ;
206- textarea . style . opacity = "0" ;
207- document . body . appendChild ( textarea ) ;
208- textarea . select ( ) ;
209- document . execCommand ( "copy" ) ;
210- document . body . removeChild ( textarea ) ;
211- }
212- setCopied ( true ) ;
213- setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
214- } ;
215-
216- return (
217- < div className = "overflow-hidden rounded-lg border border-fd-border bg-fd-card" >
218- < div className = "space-y-3 border-b border-fd-border bg-fd-muted/50 p-4" >
219- < div className = "flex flex-wrap items-center gap-3" >
220- < span className = "w-16 shrink-0 text-sm font-medium text-fd-muted-foreground" > Method</ span >
221- < ToggleGroup
222- options = { [ "binary" , "docker" , "cargo" ] as Method [ ] }
223- value = { method }
224- onChange = { setMethod }
225- labels = { { binary : "Binary" , docker : "Docker" , cargo : "Cargo" } }
226- />
227- </ div >
228- { method === "binary" && (
229- < >
230- < div className = "flex flex-wrap items-center gap-3" >
231- < span className = "w-16 shrink-0 text-sm font-medium text-fd-muted-foreground" > OS</ span >
232- < ToggleGroup
233- options = { [ "linux-x86_64" , "linux-arm64" , "macos-arm64" , "windows" ] as OS [ ] }
234- value = { os }
235- onChange = { handleOsChange }
236- labels = { osLabels }
237- />
238- </ div >
239- { isLinux && (
240- < div className = "flex flex-wrap items-center gap-3" >
241- < span className = "w-16 shrink-0 text-sm font-medium text-fd-muted-foreground" >
242- Libc
243- </ span >
244- < ToggleGroup
245- options = { [ "gnu" , "musl" ] as Libc [ ] }
246- value = { libc }
247- onChange = { handleLibcChange }
248- labels = { libcLabels }
249- disabled = { disabledLibcs }
250- />
251- </ div >
252- ) }
253- < div className = "flex flex-wrap items-center gap-3" >
254- < span className = "w-16 shrink-0 text-sm font-medium text-fd-muted-foreground" >
255- Features
256- </ span >
257- < ToggleGroup
258- options = { [ "full" , "standard" , "minimal" , "tiny" ] as Profile [ ] }
259- value = { profile }
260- onChange = { setProfile }
261- disabled = { disabledProfiles }
262- />
263- </ div >
264- </ >
265- ) }
266- </ div >
267-
268- { /* Feature matrix — shown for binary installs */ }
269- { method === "binary" && (
270- < div className = "border-b border-fd-border bg-fd-muted/20 px-4 py-3" >
271- < p className = "mb-2 text-sm font-medium text-fd-foreground" > { profileSummaries [ profile ] } </ p >
272- < div className = "grid grid-cols-2 gap-x-6 gap-y-1 sm:grid-cols-3" >
273- { featureMatrix . map ( ( f ) => {
274- const included = f . profiles . includes ( profile ) ;
275- return (
276- < div key = { f . name } className = "flex items-center gap-2 text-sm" >
277- { included ? (
278- < Check className = "h-3.5 w-3.5 shrink-0 text-green-500" />
279- ) : (
280- < X className = "h-3.5 w-3.5 shrink-0 text-fd-muted-foreground/40" />
281- ) }
282- < span
283- className = {
284- included ? "text-fd-foreground" : "text-fd-muted-foreground/50 line-through"
285- }
286- >
287- { f . name }
288- </ span >
289- </ div >
290- ) ;
291- } ) }
292- </ div >
293- </ div >
294- ) }
295-
296- < div className = "relative" >
297- < pre className = "overflow-x-auto whitespace-pre-wrap break-all p-4 pr-12 text-sm" >
298- < code className = "text-fd-foreground" > { command } </ code >
299- </ pre >
300- < button
301- onClick = { handleCopy }
302- className = "absolute right-3 top-3 rounded-md p-1.5 text-fd-muted-foreground transition-colors hover:bg-fd-muted hover:text-fd-foreground"
303- aria-label = "Copy command"
304- >
305- { copied ? < Check className = "h-4 w-4 text-green-500" /> : < Copy className = "h-4 w-4" /> }
306- </ button >
307- </ div >
308-
309- { downloadUrl && (
310- < div className = "border-t border-fd-border bg-fd-muted/30 px-4 py-3" >
311- < a
312- href = { downloadUrl }
313- className = "inline-flex items-center gap-2 rounded-lg bg-fd-primary px-4 py-2 text-sm font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/90"
314- >
315- < Download className = "h-4 w-4" />
316- Download binary
317- </ a >
318- < p className = "mt-2 break-all text-xs text-fd-muted-foreground" >
319- < a href = { downloadUrl } className = "underline" >
320- { downloadUrl }
321- </ a >
322- </ p >
323- </ div >
324- ) }
325- </ div >
326- ) ;
327- }
328-
32917// --- See it in Action (Gallery) ---
33018
33119const demos = [
0 commit comments