diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 244af24..2dc6ed5 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -62,7 +62,6 @@ jobs:
test-api:
name: API tests
needs: changes
- if: needs.changes.outputs.api == 'true'
runs-on: ubuntu-24.04-arm
timeout-minutes: 10
steps:
@@ -132,7 +131,6 @@ jobs:
helm-lint:
name: Helm chart lint
needs: changes
- if: needs.changes.outputs.helm == 'true'
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
diff --git a/frontend/components/ChatArea.tsx b/frontend/components/ChatArea.tsx
index bd58ca0..b579dc3 100644
--- a/frontend/components/ChatArea.tsx
+++ b/frontend/components/ChatArea.tsx
@@ -6,8 +6,14 @@ import { BookOpenText } from '@phosphor-icons/react/dist/ssr/BookOpenText';
import { Table } from '@phosphor-icons/react/dist/ssr/Table';
import { Compass } from '@phosphor-icons/react/dist/ssr/Compass';
import { Sparkle } from '@phosphor-icons/react/dist/ssr/Sparkle';
+import { FileArrowUp } from '@phosphor-icons/react/dist/ssr/FileArrowUp';
+import { SpinnerGap } from '@phosphor-icons/react/dist/ssr/SpinnerGap';
+import { WarningCircle } from '@phosphor-icons/react/dist/ssr/WarningCircle';
+import { ListBullets } from '@phosphor-icons/react/dist/ssr/ListBullets';
+import { Question } from '@phosphor-icons/react/dist/ssr/Question';
import { ThreadPrimitive } from '@assistant-ui/react';
import type { ChatAttachment, Message, ProjectDocument } from '@/lib/types';
+import { PROJECT_DOC_ACCEPT, PROJECT_DOC_MAX_MB, PROJECT_DOC_SUPPORTED_LABEL } from '@/lib/upload';
import MessageBubble from './MessageBubble';
import ChatInput from './ChatInput';
@@ -40,6 +46,31 @@ const SUGGESTIONS: SuggestionCard[] = [
},
];
+// Shown once a project has at least one indexed document — orients the user
+// toward their first grounded answer instead of generic tool prompts.
+const DOC_SUGGESTIONS: SuggestionCard[] = [
+ {
+ label: 'Summarize the key points',
+ query: 'Summarize the key points from my documents.',
+ icon: ,
+ },
+ {
+ label: 'Ask a question about your documents',
+ query: 'Based on my documents, ',
+ icon: ,
+ },
+ {
+ label: 'Search the knowledge base',
+ query: 'Search the knowledge base for information about ',
+ icon: ,
+ },
+ {
+ label: 'Browse the web',
+ query: 'Browse the web and find information about ',
+ icon: ,
+ },
+];
+
// RunaxAI logo: interconnected network graph with central pulse spark.
// Emerald palette to match locked brand accent (skill §3 Rule 2).
export function RunaxLogo({ size = 40 }: { size?: number }) {
@@ -93,6 +124,9 @@ interface ChatAreaProps {
onStop?: () => void;
projectId?: string;
projectDocuments?: ProjectDocument[];
+ onUploadFile?: (file: File) => void;
+ isUploading?: boolean;
+ uploadError?: string | null;
attachments?: ChatAttachment[];
onAttachmentsChange?: (next: ChatAttachment[]) => void;
sessionFileCount?: number;
@@ -110,12 +144,17 @@ export default function ChatArea({
onStop,
projectId,
projectDocuments,
+ onUploadFile,
+ isUploading = false,
+ uploadError,
attachments,
onAttachmentsChange,
sessionFileCount = 0,
sessionBytes = 0,
}: ChatAreaProps) {
const inputRef = useRef(null);
+ const heroFileInputRef = useRef(null);
+ const [heroDragOver, setHeroDragOver] = useState(false);
// Keyboard shortcuts: / to focus input, Escape to blur
useEffect(() => {
@@ -227,6 +266,25 @@ export default function ChatArea({
const isEmpty = messages.length === 0;
+ // Project-aware first-run state. In a project context we guide the user from
+ // an empty project -> first upload -> first grounded answer.
+ const isProjectContext = Boolean(projectId);
+ const docs = projectDocuments ?? [];
+ const readyDocs = docs.filter((d) => d.status === 'ready');
+ const indexingDocs = docs.filter(
+ (d) => d.status === 'processing' || d.status === 'uploading'
+ );
+ const needsFirstDocument =
+ isProjectContext && Boolean(onUploadFile) && readyDocs.length === 0;
+ const suggestions = readyDocs.length > 0 ? DOC_SUGGESTIONS : SUGGESTIONS;
+
+ const handleHeroFile = useCallback(
+ (file: File | undefined) => {
+ if (file && onUploadFile) onUploadFile(file);
+ },
+ [onUploadFile]
+ );
+
return (
{/* Messages area */}
@@ -237,7 +295,90 @@ export default function ChatArea({
aria-live="polite"
aria-label="Chat messages"
>
- {isEmpty ? (
+ {isEmpty && needsFirstDocument ? (
+ /* First-run: empty project — guide the user to upload their first doc */
+
+
+
+
+
+
+
+
+ {indexingDocs.length > 0
+ ? 'Indexing your document…'
+ : 'Add a document to get started'}
+
+
+ {indexingDocs.length > 0
+ ? 'This usually takes a few seconds. You can start chatting as soon as it’s ready.'
+ : `Upload a ${PROJECT_DOC_SUPPORTED_LABEL} file and RunaxAI will answer questions grounded in it.`}
+
+ Prefer to explore first? You can also just start chatting below — RunaxAI can
+ search the web and query databases without a document.
+
+
+
+ ) : isEmpty ? (
/* Welcome / empty state */
@@ -251,14 +392,18 @@ export default function ChatArea({
RunaxAI
- AI with full tool access — document search, database queries, and web browsing
+ {readyDocs.length > 0
+ ? `Ask anything about your ${readyDocs.length} indexed document${
+ readyDocs.length === 1 ? '' : 's'
+ } — or use web search and database queries.`
+ : 'AI with full tool access — document search, database queries, and web browsing'}