Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/strong-walls-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@gram-ai/elements": minor
---

Gram Elements is a library of UI primitives for building chat-like experiences for MCP Servers.

The first release of Gram Elements includes:

- An all-in-one `<Chat />` component that encapsulates the entire chat lifecycle, including built-in support for tool calling and streaming responses.
- A powerful configuration framework to refine the chat experience, including different layouts, theming, and much more.
5 changes: 4 additions & 1 deletion .github/filters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ cli:
- 'server/gen/**'

tsframework:
- 'ts-framework/**'
- 'ts-framework/**'

elements:
- 'elements/**'
59 changes: 59 additions & 0 deletions .github/workflows/elements-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Generate Elements TypeDoc Documentation

on:
push:
branches:
- main
paths:
- "elements/**"
workflow_dispatch: # Allow manual triggering

permissions:
contents: write

jobs:
generate-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
with:
fetch-depth: 0

- name: Setup Mise
uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1
with:
install: true
cache: true
env: false

- name: Prepare GitHub Actions environment
run: mise run github

- name: Cache PNPM
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
key: ${{ env.GH_CACHE_PNPM_KEY }}
restore-keys: |
${{ env.GH_CACHE_PNPM_KEY }}
${{ env.GH_CACHE_PNPM_KEY_PARTIAL }}
path: |
${{ env.PNPM_STORE_PATH }}

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate TypeDoc
working-directory: elements
run: pnpm run docs

- name: Commit and push if changed
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add elements/docs/
git diff --quiet && git diff --staged --quiet || (git commit -m "docs: update TypeDoc API documentation [skip ci]" && git push)

- name: Prune PNPM store
if: success()
run: pnpm store prune
66 changes: 66 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
cli: ${{ steps.gates.outputs.cli }}
functions: ${{ steps.gates.outputs.functions }}
tsframework: ${{ steps.gates.outputs.tsframework }}
elements: ${{ steps.gates.outputs.elements }}
steps:
- name: Checkout source code
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
Expand Down Expand Up @@ -74,6 +75,13 @@ jobs:
echo "TypeScript framework jobs will be skipped."
fi

if [[ "${{ steps.filter.outputs.elements }}" == "true" || "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "elements=true" >> $GITHUB_OUTPUT
echo "Elements jobs will run."
else
echo "Elements jobs will be skipped."
fi

docker-build-server:
runs-on: ubicloud-standard-4
needs: [changes, server-build-lint]
Expand Down Expand Up @@ -867,3 +875,61 @@ jobs:
- name: Prune PNPM store
if: ${{ needs.changes.outputs.tsframework == 'true' && success() }}
run: pnpm store prune

elements-build-lint:
runs-on: ubicloud-standard-4
needs: changes
steps:
- name: Skip if no elements changes exist
if: ${{ needs.changes.outputs.elements != 'true' }}
run: echo "No elements changes detected — skipping elements-build-lint."

- name: Checkout
if: ${{ needs.changes.outputs.elements == 'true' }}
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0

- name: Setup Mise
if: ${{ needs.changes.outputs.elements == 'true' }}
uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1
with:
install: true
cache: true
env: false

- name: Prepare GitHub Actions environment
if: ${{ needs.changes.outputs.elements == 'true' }}
run: mise run github

- name: Cache PNPM
if: ${{ needs.changes.outputs.elements == 'true' }}
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
key: ${{ env.GH_CACHE_PNPM_KEY }}
restore-keys: |
${{ env.GH_CACHE_PNPM_KEY }}
${{ env.GH_CACHE_PNPM_KEY_PARTIAL }}
path: |
${{ env.PNPM_STORE_PATH }}

- name: Install dependencies
if: ${{ needs.changes.outputs.elements == 'true' }}
run: pnpm install --frozen-lockfile

- name: Build
if: ${{ needs.changes.outputs.elements == 'true' }}
run: pnpm build
working-directory: elements

- name: Lint
if: ${{ needs.changes.outputs.elements == 'true' }}
run: pnpm lint
working-directory: elements

- name: Type check
if: ${{ needs.changes.outputs.elements == 'true' }}
run: pnpm type-check
working-directory: elements

- name: Prune PNPM store
if: ${{ needs.changes.outputs.elements == 'true' && success() }}
run: pnpm store prune
1 change: 1 addition & 0 deletions elements/.env.local.template
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GRAM_API_KEY=xxx
18 changes: 18 additions & 0 deletions elements/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
dist
node_modules
*storybook.log
storybook-static
*.env
.env.*

# Allow the local environment template file
!.env.local.template

# mise local overrides
.mise.local.toml

# Ignore tgz files
*.tgz

# Ignore .DS_Store
.DS_Store
1 change: 1 addition & 0 deletions elements/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--auto-install-peers=true
7 changes: 7 additions & 0 deletions elements/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss"]
}
65 changes: 65 additions & 0 deletions elements/.storybook/GlobalDecorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useMemo } from 'react'
import { ElementsProvider } from '../src/contexts/ElementsProvider'
import { ElementsConfig } from '../src/types'
import merge from 'lodash.merge'
import { recommended } from '../src/plugins'

interface ElementsDecoratorProps {
children: React.ReactNode
// Partial so stories can override only what they need
config?: Partial<ElementsConfig>
}

const DEFAULT_ELEMENTS_CONFIG: ElementsConfig = {
projectSlug: 'adamtest',
mcp: 'https://chat.speakeasy.com/mcp/speakeasy-team-my_api',
variant: 'widget',
welcome: {
title: 'Hello there!',
subtitle: 'How can I help you today?',
suggestions: [
{
title: 'Discover available tools',
label: 'Find out what tools are available',
action: 'Call all tools available',
},
],
},
composer: {
placeholder: 'Ask me anything...',
attachments: true,
},
modal: {
defaultOpen: true,
expandable: true,
defaultExpanded: true,
title: 'Gram Elements Demo',
},
tools: {
expandToolGroupsByDefault: true,
},
plugins: recommended,
}

/**
* Global decorator that wraps all stories in the AssistantRuntimeProvider,
* which provides the chat runtime to the story.
* Note: This assumes that all stories require a chat runtime, but we move back to
* per story decorator in the future.
* @param children - The children to render.
* @returns
*/
export const ElementsDecorator: React.FC<ElementsDecoratorProps> = ({
children,
config,
}) => {
const finalConfig = useMemo(
() => merge({}, DEFAULT_ELEMENTS_CONFIG, config ?? {}),
[config]
)
return (
<ElementsProvider config={finalConfig}>
<div className="h-screen bg-zinc-50">{children}</div>
</ElementsProvider>
)
}
15 changes: 15 additions & 0 deletions elements/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-docs'],
framework: {
name: '@storybook/react-vite',
options: {
builder: {
viteConfigPath: './.storybook/vite.config.mts',
},
},
},
}
export default config
28 changes: 28 additions & 0 deletions elements/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Preview } from '@storybook/react-vite'
import { ElementsDecorator } from './GlobalDecorator'
import React from 'react'
import '../src/global.css'

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
(Story, context) => {
// Stories can override config via parameters.elements
const elementsParams = context.parameters.elements ?? {}
return (
<ElementsDecorator config={elementsParams.config}>
<Story />
</ElementsDecorator>
)
},
],
}

export default preview
25 changes: 25 additions & 0 deletions elements/.storybook/vite.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig, ViteDevServer } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createElementsServerHandlers } from '../src/server'

const __dirname = dirname(fileURLToPath(import.meta.url))

const apiMiddlewarePlugin = () => ({
name: 'chat-api-middleware',
configureServer(server: ViteDevServer) {
const handlers = createElementsServerHandlers()
server.middlewares.use('/chat/completions', handlers.chat)
},
})

export default defineConfig({
plugins: [react(), tailwindcss(), apiMiddlewarePlugin()],
resolve: {
alias: {
'@': resolve(__dirname, '../src'),
},
},
})
27 changes: 27 additions & 0 deletions elements/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Contributing to `@gram-ai/elements`

## Setup

Ensure that you have your Gram API key setup in your `.env.local` file (rename the template).

Then simply run the Storybook which has a preconfigured dev middleware for chat completions so that you can test the components against a real LLM:

```bash
pnpm storybook
```

## Third party dependencies

If adding a heavy dependency, please make sure you mark it as a peer in `peerDependencies`, mark it as optional in `peerDependenciesMeta` (so the server package doesn't require it), and mark it as an `external` in the vite configuration.

## Documentation

We use TypeDoc to automatically generate markdown documentation via a github action - the documentation is generated from the TypeScript types. However you can generate documentation locally by following the guide below:

```bash
# Generate documentation
pnpm run docs

# Watch mode (regenerate on changes)
pnpm run docs:watch
```
Loading
Loading