Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0f2d514
feat: forward URL search params to vibe iframes and metadata
jchris Jul 27, 2025
40bfc7d
fix: add trailing slash to vibe iframe URLs for consistent path resol…
jchris Jul 27, 2025
8082f94
refactor: replace iframe embed with direct subdomain redirect for vib…
jchris Jul 27, 2025
33097a6
chore: bump React and ReactDOM to v19.1.1
jchris Jul 28, 2025
1f91d8c
chore: update pnpm lockfile dependencies
jchris Jul 28, 2025
da9c036
chore: update allowedHosts in Vite config for preview server
necrodome Jul 28, 2025
8d70373
feat: add X-VIBES-Token header to API key service for enhanced authen…
necrodome Jul 28, 2025
edf87f9
feat: add email support link to about page footer
jchris Jul 28, 2025
c6fa01a
fix: skip title generation for error responses in sendMessage hook
necrodome Jul 29, 2025
776d7d6
chore: format
jchris Jul 29, 2025
f97a28f
proxy for llm requests (#174)
necrodome Jul 29, 2025
a3e6a0e
chore: update dependencies and enhance test mocks for Remix, Root and…
jchris Jul 30, 2025
04befd1
feat: auto-submit chat input when prompt URL parameter is present
jchris Jul 30, 2025
549394f
feat: add chat view mode and improve prompt handling in navigation flow
jchris Jul 30, 2025
33d4751
feat: add hidden chat view type and update URL paths from /app to /chat
jchris Jul 30, 2025
755a8af
Update prompts.ts for custom color instruction
jchris Aug 4, 2025
c8f3471
feat: add editable Monaco Editor with save functionality (#190)
jchris Aug 6, 2025
64292de
feat: add MoonshotAI Kimi K2 model to available models list
jchris Aug 6, 2025
7d351ea
Remove email support link until it works
windmountain Aug 7, 2025
b35b161
Merge pull request #192 from VibesDIY/remove-help-email
windmountain Aug 7, 2025
bf042a3
Revert "Remove email support link until it works" (#193)
jchris Aug 7, 2025
c3356cf
Enhance Monaco Editor syntax error detection for save button (#191)
jchris Aug 7, 2025
7e4115d
feat: Add Storybook component library with extracted components (#197)
jchris Aug 9, 2025
5aae5bb
feat: add Storybook build and deployment configuration with caching h…
jchris Aug 9, 2025
c046dac
feat: add app settings view (#199)
necrodome Aug 9, 2025
146e2d4
refactor: make system prompt template data-driven (#195)
jchris Aug 9, 2025
f4ef45a
refactor: replace dynamic LLM text imports with static imports for be…
jchris Aug 9, 2025
a4f4cc6
chore(models): sync OpenRouter models from Issue #204 (#205)
charliecreates[bot] Aug 10, 2025
201c9e2
feat(netlify): opt-in Split Testing via nf_ab query (#207)
charliecreates[bot] Aug 11, 2025
efea8ec
feat(prompt): AI-powered LLMs.txt module selection (#202)
charliecreates[bot] Aug 11, 2025
b98e6c6
fix(ui): restore spinning Code indicator during streaming within code…
CharlieHelps Aug 11, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@

# VS Code settings
.vscode/settings.json
.claude/settings.local.json

*storybook.log
storybook-static
18 changes: 18 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@chromatic-com/storybook',
'@storybook/addon-docs',
'@storybook/addon-onboarding',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {
viteConfigPath: './.storybook/vite.config.ts',
},
},
};
export default config;
87 changes: 87 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Preview } from '@storybook/react-vite';
import '../app/app.css';

// Custom viewports for testing your app's breakpoints
const customViewports = {
xs: {
name: 'XS - Custom (480px)',
styles: { width: '480px', height: '800px' },
},
belowSm: {
name: 'Below SM (639px)',
styles: { width: '639px', height: '800px' },
},
small: {
name: 'Small Mobile (440px)',
styles: { width: '440px', height: '800px' },
},
tiny: {
name: 'Tiny (375px)',
styles: { width: '375px', height: '800px' },
},
};

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
backgrounds: {
default: 'light',
values: [
{
name: 'light',
value: '#ffffff',
},
{
name: 'dark',
value: '#1a1a1a',
},
],
},
viewport: {
viewports: customViewports,
},
},
globalTypes: {
theme: {
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => {
const theme = context.globals.theme || 'light';

// Apply theme to document for Tailwind dark mode
if (typeof document !== 'undefined') {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
document.documentElement.dataset.theme = 'dark';
} else {
document.documentElement.classList.remove('dark');
document.documentElement.dataset.theme = 'light';
}
}

return Story();
},
],
};

export default preview;
14 changes: 14 additions & 0 deletions .storybook/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import tsconfigPaths from 'vite-tsconfig-paths';

// Separate Vite config for Storybook that excludes React Router
export default defineConfig({
plugins: [tailwindcss(), tsconfigPaths()],
define: {
'process.env.DISABLE_REACT_ROUTER': '"true"',
},
json: {
stringify: true,
},
});
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Common Commands

Run `pnpm check` to format, typecheck, and test, before asking for feedback on work. Never delete a test to pass without asking.

### Development

- `pnpm dev` - Start development server with Netlify functions (primary dev command)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ Create beautiful, interactive mini apps with zero setup. Your creations are auto
- Add your AI account key from [OpenRouter](https://openrouter.ai/settings/keys)
- Run `pnpm dev`

## Developer previews on the main domain (no redirects)

Opt into an experimental branch deploy on the primary site using Netlify Split Testing with a cookie. Use `?ab=<branch>` on the main domain, for example:

```
https://vibes.diy/?ab=feature-new-ui
```

Note: the underlying Netlify cookie is named `nf_ab` and is host‑scoped by default (not shared between `www` and apex). See [docs/split-testing.md](docs/split-testing.md) for details and scope options.

## Your Work is Always Safe

Every app you create is automatically saved, so you can:
Expand Down
2 changes: 0 additions & 2 deletions __mocks__/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ export const mockUseAuth = vi.fn().mockImplementation(() => defaultAuthenticated
export const setMockAuthState = (state: Partial<AuthContextType>) => {
mockUseAuth.mockImplementation(() => ({
...defaultAuthenticatedState,
needsLogin: false,
setNeedsLogin: vi.fn(),
...state,
}));
};
Expand Down
62 changes: 39 additions & 23 deletions app/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import type { ChangeEvent, KeyboardEvent } from 'react';
import { useEffect, memo, useCallback } from 'react';
import { useEffect, memo, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
import type { ChatState } from '../types/chat';
import VibesDIYLogo from './VibesDIYLogo';
import { preloadLlmsText } from '../prompts';

interface ChatInputProps {
chatState: ChatState;
onSend: () => void;
}

function ChatInput({ chatState, onSend }: ChatInputProps) {
export interface ChatInputRef {
clickSubmit: () => void;
}

const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(({ chatState, onSend }, ref) => {
// Ref for the submit button
const submitButtonRef = useRef<HTMLButtonElement>(null);

// Expose the click function to parent components
useImperativeHandle(ref, () => ({
clickSubmit: () => {
if (submitButtonRef.current) {
submitButtonRef.current.click();
}
},
}));

// Internal callback to handle sending messages
const handleSendMessage = useCallback(() => {
if (chatState.sendMessage && !chatState.isStreaming) {
Expand All @@ -34,7 +50,7 @@ function ChatInput({ chatState, onSend }: ChatInputProps) {

return (
<div className="px-4 py-2">
<div className="relative">
<div className="space-y-1">
<textarea
ref={chatState.inputRef}
value={chatState.input}
Expand All @@ -43,6 +59,10 @@ function ChatInput({ chatState, onSend }: ChatInputProps) {
chatState.setInput(e.target.value);
}
}}
onFocus={() => {
// Fire and forget: warm the LLMs text cache using raw imports
void preloadLlmsText();
}}
onKeyDown={(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey && !chatState.isStreaming) {
e.preventDefault();
Expand All @@ -57,28 +77,24 @@ function ChatInput({ chatState, onSend }: ChatInputProps) {
}
rows={2}
/>
<button
type="button"
onClick={handleSendMessage}
disabled={chatState.isStreaming}
className={`light-gradient border-glimmer hover:border-light-decorative-01 dark:hover:border-dark-decorative-01 absolute flex items-center justify-center overflow-hidden rounded-xl border shadow-sm transition-all duration-300 hover:shadow-md active:shadow-inner ${
chatState.isStreaming
? 'border-light-decorative-01 dark:border-dark-decorative-01'
: 'border-light-decorative-01 dark:border-dark-decorative-00'
} right-0 -bottom-1 -mr-0 w-[96px] py-1`}
style={{
backdropFilter: 'blur(1px)',
}}
aria-label={chatState.isStreaming ? 'Generating' : 'Send message'}
>
<div className="relative z-10">
<VibesDIYLogo className="mr-2 mb-0.5 ml-5 pt-6 pb-2 pl-1.5" width={100} height={12} />
</div>
</button>
<div className="flex justify-end">
<button
ref={submitButtonRef}
type="button"
onClick={handleSendMessage}
disabled={chatState.isStreaming}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
aria-label={chatState.isStreaming ? 'Generating' : 'Send message'}
>
{chatState.isStreaming ? '•••' : 'Code'}
</button>
</div>
</div>
</div>
);
}
});

ChatInput.displayName = 'ChatInput';

// Use memo to optimize rendering
export default memo(ChatInput);
5 changes: 3 additions & 2 deletions app/components/CookieBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { GA_TRACKING_ID } from '../config/env';
import { useCookieConsent } from '../contexts/CookieConsentContext';
import { useTheme } from '../contexts/ThemeContext';
import { initGA, pageview } from '../utils/analytics';

// We'll use any type for dynamic imports to avoid TypeScript errors with the cookie consent component
Expand All @@ -12,7 +11,9 @@ export default function CookieBanner() {
const location = useLocation();
const [hasConsent, setHasConsent] = useState(false);
const { messageHasBeenSent } = useCookieConsent();
const { isDarkMode } = useTheme();
// Use CSS-based dark mode detection like the rest of the UI
const isDarkMode =
typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : true; // Default to dark mode for SSR

// Dynamic import for client-side only
const [CookieConsent, setCookieConsent] = useState<any>(null);
Expand Down
53 changes: 53 additions & 0 deletions app/components/HeaderContent/SvgIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ export const DataIcon: React.FC<SvgIconProps> = ({
);
};

export const SettingsIcon: React.FC<SvgIconProps> = ({ className = 'h-4 w-4', title }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>{title || 'Settings icon'}</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
);
};

export const ShareIcon: React.FC<SvgIconProps> = ({ className = 'h-4 w-4', title }) => {
return (
<svg
Expand Down Expand Up @@ -233,3 +259,30 @@ export const PublishIcon: React.FC<SvgIconProps> = ({ className = 'h-5 w-5' }) =
</svg>
);
};

export const MinidiscIcon: React.FC<SvgIconProps> = ({ className = 'h-4 w-4', title }) => {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
<title>{title || 'Save icon (minidisc)'}</title>
{/* Minidisc outline */}
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="1.5" />
{/* Inner ring */}
<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" strokeWidth="1" />
{/* Label area */}
<rect
x="4"
y="8"
width="6"
height="8"
rx="1"
fill="none"
stroke="currentColor"
strokeWidth="1"
/>
{/* Label lines */}
<line x1="5" y1="10" x2="9" y2="10" stroke="currentColor" strokeWidth="0.5" />
<line x1="5" y1="12" x2="8" y2="12" stroke="currentColor" strokeWidth="0.5" />
<line x1="5" y1="14" x2="9" y2="14" stroke="currentColor" strokeWidth="0.5" />
</svg>
);
};
2 changes: 1 addition & 1 deletion app/components/NeedsLoginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useAuthPopup } from '../hooks/useAuthPopup';

/**
* A modal that appears when the user needs to login to get more credits
* This listens for the 'needsLoginTriggered' event from useSimpleChat
* This listens for the needsLogin state from AuthContext
*/
export function NeedsLoginModal() {
const [isOpen, setIsOpen] = useState(false);
Expand Down
Loading