diff --git a/README.md b/README.md index 9bdf117..f6d49a0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,86 @@ The application uses the following technologies: Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly. +## Multi-Provider Support + +This application supports multiple AI providers, making it easy to deploy with your preferred service. Choose from: + +### Supported Providers + +| Provider | Status | Best For | +|----------|--------|----------| +| **AWS Bedrock** | ✅ Default | Claude models via AWS infrastructure | +| **OpenAI** | ✅ Supported | GPT-4, GPT-5, and reasoning models | +| **Anthropic** | ✅ Supported | Direct access to Claude models | +| **Google AI** | ✅ Supported | Gemini models with multi-modal capabilities | +| **Azure OpenAI** | ✅ Supported | Enterprise OpenAI deployments | +| **Ollama** | ✅ Supported | Local/self-hosted open source models | + +### Quick Setup by Provider + +#### AWS Bedrock (Default) +```bash +AI_PROVIDER=bedrock +AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0 +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=your-access-key +AWS_SECRET_ACCESS_KEY=your-secret-key +``` + +#### OpenAI +```bash +AI_PROVIDER=openai +AI_MODEL=gpt-4o +OPENAI_API_KEY=sk-... +``` + +#### Anthropic +```bash +AI_PROVIDER=anthropic +AI_MODEL=claude-sonnet-4-5 +ANTHROPIC_API_KEY=sk-ant-... +``` + +#### Google Generative AI +```bash +AI_PROVIDER=google +AI_MODEL=gemini-2.5-flash +GOOGLE_GENERATIVE_AI_API_KEY=... +``` + +#### Azure OpenAI +```bash +AI_PROVIDER=azure +AI_MODEL=your-deployment-name +AZURE_RESOURCE_NAME=your-resource +AZURE_API_KEY=... +``` + +#### Ollama (Local) +```bash +AI_PROVIDER=ollama +AI_MODEL=phi3 +OLLAMA_BASE_URL=http://localhost:11434/api # Optional +``` +Note: Install models locally first with `ollama pull ` + +### Recommended Models + +**Best Quality:** +- AWS Bedrock: `global.anthropic.claude-sonnet-4-5-20250929-v1:0` +- Anthropic: `claude-sonnet-4-5` +- OpenAI: `gpt-4o` or `gpt-5` + +**Best Speed:** +- Google: `gemini-2.5-flash` +- OpenAI: `gpt-4o` +- Anthropic: `claude-haiku-4-5` + +**Best Cost:** +- Ollama: Free (local models) +- Google: `gemini-1.5-flash-8b` +- OpenAI: `gpt-4o-mini` + ## Getting Started ### Installation @@ -45,15 +125,20 @@ npm install yarn install ``` -3. Create a `.env.local` file in the root directory. You can use `env.example` as a template: +3. Configure your AI provider: + +Create a `.env.local` file in the root directory: ```bash -cp env.example .env.local +cp .env.example .env.local ``` -Then update `.env.local` with your actual API keys: +Edit `.env.local` and configure your chosen provider: +- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama) +- Set `AI_MODEL` to the specific model you want to use +- Add the required API keys for your provider -Note: Not all variables are required. At minimum, you'll need at least one AI provider API key (OpenAI, Google, or OpenRouter). +See the [Multi-Provider Support](#multi-provider-support) section above for provider-specific configuration examples. 4. Run the development server: @@ -90,7 +175,7 @@ public/ # Static assets including example images - [x] Allow the LLM to modify the XML instead of generating it from scratch everytime. - [x] Improve the smoothness of shape streaming updates. -- [ ] Add multiple AI provider support (Google PaLM, Anthropic Claude, etc.) +- [x] Add multiple AI provider support (OpenAI, Anthropic, Google, Azure, Ollama) ## License diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 5183e9b..9da2f28 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,16 +1,8 @@ -import { bedrock } from '@ai-sdk/amazon-bedrock'; -import { openai } from '@ai-sdk/openai'; -import { google } from '@ai-sdk/google'; -import { smoothStream, streamText, convertToModelMessages } from 'ai'; -import { createOpenRouter } from '@openrouter/ai-sdk-provider'; -import { createGoogleGenerativeAI } from '@ai-sdk/google'; -import { createOpenAI } from '@ai-sdk/openai'; +import { streamText, convertToModelMessages } from 'ai'; +import { getAIModel } from '@/lib/ai-providers'; +import { z } from "zod"; -import { z } from "zod/v3"; -import { replaceXMLParts } from "@/lib/utils"; - -export const maxDuration = 60 -const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }); +export const maxDuration = 60; export async function POST(req: Request) { try { @@ -127,34 +119,14 @@ ${lastMessageText} console.log("Enhanced messages:", enhancedMessages); + // Get AI model from environment configuration + const { model, providerOptions } = getAIModel(); + const result = streamText({ - // model: google("gemini-2.5-flash-preview-05-20"), - // model: google("gemini-2.5-pro"), - // model: bedrock('anthropic.claude-sonnet-4-20250514-v1:0'), + model, system: systemMessage, - model: bedrock('global.anthropic.claude-sonnet-4-5-20250929-v1:0'), - // model: openrouter('moonshotai/kimi-k2:free'), - // model: model, - // providerOptions: { - // google: { - // thinkingConfig: { - // thinkingBudget: 128, - // }, - // } - // }, - // providerOptions: { - // openai: { - // reasoningEffort: "minimal" - // }, - // }, - providerOptions: { - anthropic: { - additionalModelRequestFields: { - anthropic_beta: ['fine-grained-tool-streaming-2025-05-14'] - } - } - }, messages: enhancedMessages, + ...(providerOptions && { providerOptions }), tools: { // Client-side tool that will be executed on the client display_diagram: { diff --git a/components/chat-input.tsx b/components/chat-input.tsx index b58a34a..e59dc32 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -45,6 +45,12 @@ export function ChatInput({ const [isDragging, setIsDragging] = useState(false); const [showClearDialog, setShowClearDialog] = useState(false); + // Debug: Log status changes + const isDisabled = status === "streaming" || status === "submitted"; + useEffect(() => { + console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled); + }, [status, isDisabled]); + // Auto-resize textarea based on content const adjustTextareaHeight = useCallback(() => { const textarea = textareaRef.current; @@ -63,7 +69,7 @@ export function ChatInput({ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); const form = e.currentTarget.closest("form"); - if (form && input.trim() && status !== "streaming") { + if (form && input.trim() && !isDisabled) { form.requestSubmit(); } } @@ -71,7 +77,7 @@ export function ChatInput({ // Handle clipboard paste const handlePaste = async (e: React.ClipboardEvent) => { - if (status === "streaming") return; + if (isDisabled) return; const items = e.clipboardData.items; const imageItems = Array.from(items).filter((item) => @@ -140,7 +146,7 @@ export function ChatInput({ e.stopPropagation(); setIsDragging(false); - if (status === "streaming") return; + if (isDisabled) return; const droppedFiles = e.dataTransfer.files; @@ -183,7 +189,7 @@ export function ChatInput({ placeholder="Describe what changes you want to make to the diagram or upload(paste) an image to replicate a diagram. (Press Cmd/Ctrl + Enter to send)" - disabled={status === "streaming"} + disabled={isDisabled} aria-label="Chat input" className="min-h-[80px] resize-none transition-all duration-200 px-1 py-0" /> @@ -220,7 +226,7 @@ export function ChatInput({ size="icon" onClick={() => onToggleHistory(true)} disabled={ - status === "streaming" || + isDisabled || diagramHistory.length === 0 } title="Diagram History" @@ -234,7 +240,7 @@ export function ChatInput({ variant="outline" size="icon" onClick={triggerFileInput} - disabled={status === "streaming"} + disabled={isDisabled} title="Upload image" > @@ -247,21 +253,21 @@ export function ChatInput({ onChange={handleFileChange} accept="image/*" multiple - disabled={status === "streaming"} + disabled={isDisabled} />