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
22 changes: 19 additions & 3 deletions apps/python-service/routers/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,29 @@ class CollectionField(BaseModel):
required: bool = False
ref: str | None = None
unique: bool = False
default: Union[str, int, float, bool, None] = None
items: dict | None = None
fields: list["CollectionField"] | None = None

@model_validator(mode='after')
def validate_object_fields(self):
def validate_field(self):
if self.type == "Object":
if self.fields is None or len(self.fields) == 0:
raise ValueError("fields must be present and non-empty for Object type")

if self.default is not None:
if self.required:
raise ValueError("default is not allowed on required fields")
if self.type not in ["String", "Number", "Boolean"]:
raise ValueError(f"default is not allowed on {self.type} fields")

if self.type == "Boolean" and not isinstance(self.default, bool):
raise ValueError("default must be boolean for Boolean fields")
elif self.type == "Number" and not (isinstance(self.default, (int, float)) and not isinstance(self.default, bool)):
raise ValueError("default must be numeric for Number fields")
elif self.type == "String" and not isinstance(self.default, str):
raise ValueError("default must be string for String fields")

return self

class CollectionSchema(BaseModel):
Expand Down Expand Up @@ -182,8 +197,9 @@ async def collection_creator(request: CollectionCreatorRequest, req: Request):
1. If the description is vague, ask 2-3 specific clarifying questions and set type: "clarify". You MUST set "schema": null. NEVER ask more than 3 at once.
2. Once you have enough context, propose a schema and set type: "schema".
3. Only use these field types: String, Number, Boolean, Date, Ref, Array, Object. For Ref, ALWAYS set 'ref' to the referenced collection name e.g. 'users'. For Array, set 'items' to describe item type e.g. {"type": "String"}. For Object, set 'fields' as a list of sub-fields. Do NOT use Object as a lazy catch-all — prefer specific flat fields when possible.
4. ALWAYS include "createdAt" (type: Date, required: true) in EVERY collection.
5. Suggest Ref to "users" where ownership applies (ownerId, authorId, userId, etc.) with 'ref': 'users'.
4. If a field is NOT required, you may suggest a default value using the 'default' key (value type must exactly match the field type). Do not provide defaults for required fields or complex types.
5. ALWAYS include "createdAt" (type: Date, required: true) in EVERY collection.
6. Suggest Ref to "users" where ownership applies (ownerId, authorId, userId, etc.) with 'ref': 'users'.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
6. NEVER generate a collection named "users" — it is reserved for auth.
7. When the developer confirms satisfaction ("looks good", "yes", "create it", "perfect") -> set type: "complete", set "schema": null, and in your message, clearly tell the developer that their schema is finalized and guide them to click the "Insert All" button in the Schema Preview panel on the right to create the collections in their database. NEVER claim that you have already created the collections yourself.
8. The "schema" field MUST ALWAYS BE AN ARRAY (LIST) OF OBJECTS. Even if you are proposing a single collection, you must wrap it in an array like: `"schema": [ { "collection": "...", "fields": [...] } ]`. If `type` is "clarify" or "complete", you MUST set `"schema": null`.
Expand Down
1 change: 1 addition & 0 deletions apps/web-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-tabs": "^1.1.21",
Expand Down
48 changes: 29 additions & 19 deletions apps/web-dashboard/src/components/CollectionCreatorAgent.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import api from '../utils/api';
import { toast } from 'react-hot-toast';
import { Send, ArrowRight } from 'lucide-react';
import { Send, ArrowRight, Bot } from 'lucide-react';
import SchemaCanvasViewer from './SchemaCanvasViewer';

const SUGGESTIONS = [
Expand Down Expand Up @@ -166,6 +166,9 @@ export default function CollectionCreatorAgent({ projectId, onInsertAll }) {
if (f.unique !== undefined && !['Array', 'Object', 'Ref'].includes(f.type)) {
fieldDef.unique = !!f.unique;
}
if (f.default !== undefined) {
fieldDef.default = f.default;
}
return fieldDef;
});
};
Expand Down Expand Up @@ -214,14 +217,22 @@ export default function CollectionCreatorAgent({ projectId, onInsertAll }) {
};

return (
<div className="flex flex-col lg:flex-row gap-8 h-full w-full font-sans overflow-hidden">
<div className="grid grid-cols-1 lg:grid-cols-[42%_1fr] gap-4 h-full w-full font-sans overflow-hidden">

{/* Open Chat Stream & Pinned Composer - Left */}
<div className="flex flex-col flex-1 h-full min-w-0 overflow-hidden relative">
{/* AI Assistant Chat Panel - Left */}
<div className="flex flex-col h-full min-w-0 rounded-xl overflow-hidden shadow-sm border border-[var(--color-border)] bg-[var(--color-bg-card)] relative">

{/* Title Bar matching right side */}
<div className="p-3 px-5 border-b border-[var(--color-border)] bg-[var(--color-bg-card)] flex justify-between items-center flex-shrink-0 gap-4 min-h-[56px] z-10">
<h3 className="text-sm font-semibold text-[var(--color-text-main)] flex items-center gap-2.5 m-0">
<Bot size={16} className="text-[var(--color-primary)]" />
<span>AI Schema Assistant</span>
</h3>
</div>

{/* Messages Stream (Only this scrolls) */}
<div
className="flex-1 overflow-y-auto custom-scrollbar pr-3 flex flex-col gap-4 min-h-0"
className="flex-1 overflow-y-auto custom-scrollbar p-4 flex flex-col gap-5 min-h-0"
>
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
Expand All @@ -248,30 +259,29 @@ export default function CollectionCreatorAgent({ projectId, onInsertAll }) {

{/* Quick Start Suggestions on empty chat */}
{messages.length <= 1 && !schema && (
<div className="mt-2 flex flex-col gap-2.5">
<span className="text-[11px] font-semibold text-[var(--color-text-muted)] uppercase tracking-wider pl-1">
<div className="mt-5 flex flex-col gap-2.5">
<span className="text-xs font-semibold text-[var(--color-text-muted)] uppercase tracking-wider mb-1">
Quick Start Ideas
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<div className="flex flex-wrap gap-2">
{SUGGESTIONS.map((s, i) => (
<button
key={i}
type="button"
onClick={() => sendMessage(null, s.prompt)}
disabled={aiStatus === 'loading' || iterationsLeft === 0}
className="text-left p-3 px-3.5 bg-[var(--color-bg-card)] border border-[var(--color-border)] hover:border-[var(--color-primary)] rounded-xl text-xs font-medium text-[var(--color-text-main)] flex items-center justify-between gap-3 transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow"
className="text-left px-4 py-2 bg-[var(--color-bg-input)] border border-[var(--color-border)] hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5 rounded-full text-xs font-medium text-[var(--color-text-main)] hover:text-[var(--color-primary)] transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
>
<span className="truncate">{s.label}</span>
<ArrowRight size={13} className="opacity-50 shrink-0" />
{s.label}
</button>
))}
</div>
</div>
)}

{aiStatus === 'loading' && (
<div className="flex justify-start">
<div className="bg-[var(--color-bg-card)] p-3 px-4 rounded-xl rounded-bl-sm border border-[var(--color-border)] flex items-center gap-3 shadow-sm">
<div className="flex justify-start mt-2">
<div className="bg-[rgba(255,255,255,0.03)] p-3 px-4 rounded-xl rounded-bl-sm border border-[var(--color-border)] flex items-center gap-3 shadow-sm">
<div className="spinner-small w-4 h-4 border-t-[var(--color-primary)]"></div>
<span className="text-xs text-[var(--color-text-muted)] font-medium">
AI is designing your schema...
Expand All @@ -283,29 +293,29 @@ export default function CollectionCreatorAgent({ projectId, onInsertAll }) {
</div>

{/* Pinned Bottom Composer */}
<div className="mt-auto pt-3 flex-shrink-0">
<div className="px-4 pb-4 pt-2 bg-[var(--color-bg-card)] flex-shrink-0">
<div
className="relative flex items-end bg-[var(--color-bg-card)] border border-[var(--color-border)] rounded-xl p-2.5 px-4 shadow-lg transition-all focus-within:border-[var(--color-primary)] focus-within:ring-1 focus-within:ring-[var(--color-primary)]/30"
className="relative flex items-end bg-[var(--color-bg-input)] border border-[var(--color-border)] rounded-xl p-2 pl-4 shadow-sm transition-all focus-within:border-[var(--color-primary)] focus-within:ring-1 focus-within:ring-[var(--color-primary)]/30"
>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
disabled={aiStatus === 'loading' || (iterationsLeft === 0)}
placeholder={iterationsLeft === 0 ? "Turn limit reached. Add Groq key in Settings." : "Describe your app (e.g. e-commerce store, booking system, CRM...)"}
placeholder={iterationsLeft === 0 ? "Turn limit reached. Add Groq key in Settings." : "Describe your app (e.g. e-commerce store, CRM)..."}
aria-label="Message the schema assistant"
rows={1}
className="w-full bg-transparent border-0 outline-none text-[var(--color-text-main)] text-sm leading-relaxed resize-none max-h-[110px] min-h-[26px] py-1 placeholder:text-[var(--color-text-muted)]"
className="w-full bg-transparent border-0 outline-none text-[var(--color-text-main)] text-sm leading-relaxed resize-none max-h-[110px] min-h-[26px] py-1.5 pl-2 pr-10 placeholder:text-[var(--color-text-muted)]"
/>
<button
type="button"
onClick={sendMessage}
disabled={!inputValue.trim() || aiStatus === 'loading' || (iterationsLeft === 0)}
className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--color-primary)] text-black border-0 cursor-pointer disabled:opacity-35 disabled:cursor-not-allowed transition-all shrink-0 ml-2.5 font-semibold"
className="absolute right-3 bottom-2.5 flex items-center justify-center w-8 h-8 rounded-xl bg-[var(--color-primary)] text-black border-0 cursor-pointer disabled:opacity-35 disabled:cursor-not-allowed transition-all font-semibold shadow-sm"
title="Send message (Enter)"
>
<Send size={14} />
<Send size={15} />
</button>
</div>
{getIterationsBadge()}
Expand Down
7 changes: 4 additions & 3 deletions apps/web-dashboard/src/components/SchemaCanvasViewer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default function SchemaCanvasViewer({
type: f.type,
required: !!f.required,
unique: !!f.unique,
default: f.default,
ref: f.ref,
items: f.items,
fields: f.fields
Expand Down Expand Up @@ -175,7 +176,7 @@ export default function SchemaCanvasViewer({
return (
<TooltipProvider>
<div
className="flex flex-col flex-1 lg:flex-[1.25] h-full min-w-0 rounded-2xl overflow-hidden shadow-sm border border-[var(--color-border)] bg-[var(--color-bg-card)] relative"
className="flex flex-col h-full min-w-0 rounded-xl overflow-hidden shadow-sm border border-[var(--color-border)] bg-[var(--color-bg-card)] relative"
>
{/* Top Header & Tabs Toolbar (Spacious & Clean) */}
<div className="p-3 px-5 border-b border-[var(--color-border)] bg-[var(--color-bg-card)] flex justify-between items-center flex-shrink-0 gap-4 flex-wrap z-10 min-h-[56px]">
Expand Down Expand Up @@ -291,8 +292,8 @@ export default function SchemaCanvasViewer({
<p className="text-xs text-[var(--color-text-muted)] max-w-xs leading-relaxed mb-4">
Describe your application idea in the chat on the left. The AI architect will design your MongoDB collections and render an interactive canvas here.
</p>
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--color-bg-input)] border border-[var(--color-border)] text-[11px] text-[var(--color-text-muted)] font-mono">
<span>Example: "A multi-tenant SaaS with workspaces, projects, and invoices"</span>
<div className="text-[11px] text-[var(--color-text-muted)] opacity-70 italic">
Example: "A multi-tenant SaaS with workspaces, projects, and invoices"
</div>
</div>
) : viewMode === 'canvas' ? (
Expand Down
25 changes: 25 additions & 0 deletions apps/web-dashboard/src/components/ui/checkbox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"

import { cn } from "../../lib/utils"

const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-[var(--color-border)] bg-[var(--color-bg-input)] shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-primary)] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-[var(--color-primary)] data-[state=checked]:text-black",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-3 w-3 font-bold" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
19 changes: 19 additions & 0 deletions apps/web-dashboard/src/components/ui/input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "../../lib/utils"

const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg-input)] px-3 py-2 text-sm text-[var(--color-text-main)] shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[var(--color-text-muted)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-primary)] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"

export { Input }
10 changes: 6 additions & 4 deletions apps/web-dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,12 @@
box-shadow: none !important;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}

body {
Expand Down
Loading
Loading