Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions starter/c1-thesys-dev/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": true,
"extends": "next/core-web-vitals"
}
42 changes: 42 additions & 0 deletions starter/c1-thesys-dev/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
/node_modules
/.pnp
.pnp.js

# Testing
/coverage

# Next.js
/.next/
/out/
next-env.d.ts

# Production
build
dist

# Misc
.DS_Store
*.pem

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local ENV files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Turborepo
.turbo

# typescript
*.tsbuildinfo
64 changes: 64 additions & 0 deletions starter/c1-thesys-dev/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
name: C1 by Thesys Generative UI Starter
slug: c1-thesys-genui-start
description: Starter template for generative UI with C1 by Thesys
framework: Next.js
useCase: Starter
css: Tailwind
deployUrl: https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/starter/c1-thesys-dev&project-name=c1-thesys-dev&repository-name=c1-thesys-dev
demoUrl: https://c1-thesys-starter.vercel.app/
---

# C1 by Thesys App Example

Starter template for a generative UI app, powered by [C1 by Thesys](https://thesys.dev)

[![Built with Thesys](https://thesys.dev/built-with-thesys-badge.svg)](https://thesys.dev)

## Demo

https://c1-thesys-starter.vercel.app/

## Getting Started

First, generate a new API key from [Thesys Console](https://console.thesys.dev/keys) and then set it your environment variable.

```bash
export THESYS_API_KEY=<your-api-key>
```

Install dependencies:

```bash
pnpm i
```

Then, run the development server:

```bash
pnpm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing your responses by modifying the system prompt in `app/api/chat/route.ts`.

## Learn More

To learn more about Thesys C1, take a look at the [C1 Documentation](https://docs.thesys.dev) - learn about Thesys C1.

### One-Click Deploy

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/starter/c1-thesys-dev&project-name=c1-thesys-dev&repository-name=c1-thesys-dev)

### Clone and Deploy

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
pnpm create next-app --example https://github.com/vercel/examples/tree/main/starter/c1-thesys-dev
```

Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=edge-middleware-eap) ([Documentation](https://nextjs.org/docs/deployment)).
48 changes: 48 additions & 0 deletions starter/c1-thesys-dev/app/api/ask/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextRequest } from 'next/server'
import OpenAI from 'openai'
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
import { transformStream } from '@crayonai/stream'

const client = new OpenAI({
baseURL: 'https://api.thesys.dev/v1/embed',
apiKey: process.env.THESYS_API_KEY,
})

export async function POST(req: NextRequest) {
const { prompt, previousC1Response } = (await req.json()) as {
prompt: string
previousC1Response?: string
}

const messages: ChatCompletionMessageParam[] = []

if (previousC1Response) {
messages.push({
role: 'assistant',
content: previousC1Response,
})
}

messages.push({
role: 'user',
content: prompt,
})

const llmStream = await client.chat.completions.create({
model: 'c1/anthropic/claude-sonnet-4/v-20250815',
messages: [...messages],
stream: true,
})

const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0]?.delta?.content || ''
})

return new Response(responseStream as ReadableStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
})
}
24 changes: 24 additions & 0 deletions starter/c1-thesys-dev/app/components/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const Loader = () => {
return (
<svg
className="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)
}
Binary file added starter/c1-thesys-dev/app/favicon.ico
Binary file not shown.
26 changes: 26 additions & 0 deletions starter/c1-thesys-dev/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import 'tailwindcss';

:root {
--background: #ffffff;
--foreground: #171717;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
91 changes: 91 additions & 0 deletions starter/c1-thesys-dev/app/helpers/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Type definition for parameters required by the makeApiCall function.
* This includes both the API request parameters and state management callbacks.
*/
export type ApiCallParams = {
/** The search query to be sent to the API */
searchQuery: string
/** Optional previous response for context in follow-up queries */
previousC1Response?: string
/** Callback to update the response state */
setC1Response: (response: string) => void
/** Callback to update the loading state */
setIsLoading: (isLoading: boolean) => void
/** Current abort controller for cancelling ongoing requests */
abortController: AbortController | null
/** Callback to update the abort controller state */
setAbortController: (controller: AbortController | null) => void
}

/**
* Makes an API call to the /api/ask endpoint with streaming response handling.
* Supports request cancellation and manages loading states.
*
* @param params - Object containing all necessary parameters and callbacks
*/
export const makeApiCall = async ({
searchQuery,
previousC1Response,
setC1Response,
setIsLoading,
abortController,
setAbortController,
}: ApiCallParams) => {
try {
// Cancel any ongoing request before starting a new one
if (abortController) {
abortController.abort()
}

// Create and set up a new abort controller for this request
const newAbortController = new AbortController()
setAbortController(newAbortController)
setIsLoading(true)

// Make the API request with the abort signal
const response = await fetch('/api/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: searchQuery,
previousC1Response,
}),
signal: newAbortController.signal,
})

// Set up stream reading utilities
const decoder = new TextDecoder()
const stream = response.body?.getReader()

if (!stream) {
throw new Error('response.body not found')
}

// Initialize accumulator for streamed response
let streamResponse = ''

// Read the stream chunk by chunk
while (true) {
const { done, value } = await stream.read()
// Decode the chunk, considering if it's the final chunk
const chunk = decoder.decode(value, { stream: !done })

// Accumulate response and update state
streamResponse += chunk
setC1Response(streamResponse)

// Break the loop when stream is complete
if (done) {
break
}
}
} catch (error) {
console.error('Error in makeApiCall:', error)
} finally {
// Clean up: reset loading state and abort controller
setIsLoading(false)
setAbortController(null)
}
}
67 changes: 67 additions & 0 deletions starter/c1-thesys-dev/app/hooks/useUIState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useState } from 'react'
import { makeApiCall } from '../helpers/api'

/**
* Type definition for the UI state.
* Contains all the state variables needed for the application's UI.
*/
export type UIState = {
/** The current search query input */
query: string
/** The current response from the C1 API */
c1Response: string
/** Whether an API request is currently in progress */
isLoading: boolean
}

/**
* Custom hook for managing the application's UI state.
* Provides a centralized way to manage state and API interactions.
*
* @returns An object containing:
* - state: Current UI state
* - actions: Functions to update state and make API calls
*/
export const useUIState = () => {
// State for managing the search query input
const [query, setQuery] = useState('')
// State for storing the API response
const [c1Response, setC1Response] = useState('')
// State for tracking if a request is in progress
const [isLoading, setIsLoading] = useState(false)
// State for managing request cancellation
const [abortController, setAbortController] =
useState<AbortController | null>(null)

/**
* Wrapper function around makeApiCall that provides necessary state handlers.
* This keeps the component interface simple while handling all state management internally.
*/
const handleApiCall = async (
searchQuery: string,
previousC1Response?: string
) => {
await makeApiCall({
searchQuery,
previousC1Response,
setC1Response,
setIsLoading,
abortController,
setAbortController,
})
}

// Return the state and actions in a structured format
return {
state: {
query,
c1Response,
isLoading,
},
actions: {
setQuery,
setC1Response,
makeApiCall: handleApiCall,
},
}
}
24 changes: 24 additions & 0 deletions starter/c1-thesys-dev/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({
subsets: ['latin'],
})

export const metadata: Metadata = {
title: 'C1 Chat',
description: 'Generative UI App powered by Thesys C1',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
)
}
Loading