From 420ee88b4bc7942c9badd354783f67bbc9a84a4c Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Tue, 17 Mar 2026 22:46:14 +0300 Subject: [PATCH 1/5] feat(backend): add answer_question to AIActionType enum --- backend/app/models/ai_interaction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/models/ai_interaction.py b/backend/app/models/ai_interaction.py index 9a0075b..34d373f 100644 --- a/backend/app/models/ai_interaction.py +++ b/backend/app/models/ai_interaction.py @@ -21,6 +21,7 @@ class AIActionType(enum.Enum): analogy = "analogy" example = "example" expand_acronym = "expand_acronym" + answer_question = "answer_question" class AIInteraction(Base): From 8cfb12eadb40b336b11460217ca616eb81fcfa19 Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Tue, 17 Mar 2026 22:46:29 +0300 Subject: [PATCH 2/5] feat(backend): add prompt template for answering highlighted questions --- backend/app/prompts/highlight/answer_question.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/app/prompts/highlight/answer_question.md diff --git a/backend/app/prompts/highlight/answer_question.md b/backend/app/prompts/highlight/answer_question.md new file mode 100644 index 0000000..fdda6a9 --- /dev/null +++ b/backend/app/prompts/highlight/answer_question.md @@ -0,0 +1,12 @@ +DOCUMENT CONTEXT: +{{context}} + +QUESTION: +{{selected_text}} + +TASK: +Answer the selected question based on the document context provided. +If the context doesn't contain the answer, state that clearly instead of guessing. +Strict Constraint: Provide the answer immediately. Do not say "Sure", "Here is the answer", or "Based on the text". Start with the core answer. + +ANSWER: From b39a71268abc65d20fde21b7ce49102a2f9ea3f3 Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Tue, 17 Mar 2026 22:46:43 +0300 Subject: [PATCH 3/5] feat(frontend): add Answer Question action to aiActions menu --- frontend/src/components/study-room/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/study-room/constants.ts b/frontend/src/components/study-room/constants.ts index a54dfa6..b7023ff 100644 --- a/frontend/src/components/study-room/constants.ts +++ b/frontend/src/components/study-room/constants.ts @@ -1,4 +1,4 @@ -import { BookOpen, TextQuote, Lightbulb, MessageSquare, Hash } from 'lucide-react'; +import { BookOpen, TextQuote, Lightbulb, MessageSquare, Hash, HelpCircle } from 'lucide-react'; export const aiActions = [ { key: 'explain', label: 'Explain Simple', icon: BookOpen }, @@ -6,6 +6,7 @@ export const aiActions = [ { key: 'example', label: 'Give Example', icon: Lightbulb }, { key: 'analogy', label: 'Analogy', icon: MessageSquare }, { key: 'acronym', label: 'Extend Acronym', icon: Hash }, + { key: 'question', label: 'Answer Question', icon: HelpCircle }, ] as const; export type ActionKey = (typeof aiActions)[number]['key']; From 0cbb9811746d348247ac7c1885cdbb81fc93b5d8 Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Tue, 17 Mar 2026 22:47:12 +0300 Subject: [PATCH 4/5] feat(frontend): map question action to backend answer_question endpoint --- frontend/src/services/ai.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/services/ai.service.ts b/frontend/src/services/ai.service.ts index 5c6d972..b948ad2 100644 --- a/frontend/src/services/ai.service.ts +++ b/frontend/src/services/ai.service.ts @@ -9,6 +9,7 @@ const ACTION_MAP: Record = { example: 'example', analogy: 'analogy', acronym: 'expand_acronym', + question: 'answer_question', }; /** From 677cf6fc10cf6123cdbbe00a3df4b96c9f3ed26e Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Fri, 20 Mar 2026 16:11:21 +0300 Subject: [PATCH 5/5] fix: resolve excution error and align dev env --- .gitattributes | 1 + .../6e2d1d2b7c4a_add_ai_interactions.py | 54 +++++++++++++++++++ backend/requirements.txt | 2 +- docker-compose.dev.yml | 24 +++++++++ frontend/Dockerfile | 6 ++- 5 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 backend/alembic/versions/6e2d1d2b7c4a_add_ai_interactions.py create mode 100644 docker-compose.dev.yml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4c98f85 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +backend/start.sh text eol=lf diff --git a/backend/alembic/versions/6e2d1d2b7c4a_add_ai_interactions.py b/backend/alembic/versions/6e2d1d2b7c4a_add_ai_interactions.py new file mode 100644 index 0000000..6656cb4 --- /dev/null +++ b/backend/alembic/versions/6e2d1d2b7c4a_add_ai_interactions.py @@ -0,0 +1,54 @@ +"""Add ai_interactions table + +Revision ID: 6e2d1d2b7c4a +Revises: af24fc97a432 +Create Date: 2026-03-20 15:50:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '6e2d1d2b7c4a' +down_revision: Union[str, Sequence[str], None] = 'af24fc97a432' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create Enum types + # Note: Using sa.Enum in create_table will automatically create the type if it doesn't exist, + # but for PostgreSQL it's often better to create it explicitly if we want to be safe. + + op.create_table('ai_interactions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('document_id', sa.UUID(), nullable=False), + sa.Column('interaction_type', sa.Enum('highlight', 'question', 'summary', 'quiz', name='aiinteractiontype'), nullable=False), + sa.Column('action', sa.Enum('explain_simple', 'define', 'analogy', 'example', 'expand_acronym', 'answer_question', name='aiactiontype'), nullable=False), + sa.Column('input_text', sa.Text(), nullable=False), + sa.Column('response_text', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['document_id'], ['documents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_ai_interactions_document_id'), 'ai_interactions', ['document_id'], unique=False) + op.create_index(op.f('ix_ai_interactions_user_id'), 'ai_interactions', ['user_id'], unique=False) + op.create_index('idx_user_document', 'ai_interactions', ['user_id', 'document_id'], unique=False) + + +def downgrade() -> None: + op.drop_index('idx_user_document', table_name='ai_interactions') + op.drop_index(op.f('ix_ai_interactions_user_id'), table_name='ai_interactions') + op.drop_index(op.f('ix_ai_interactions_document_id'), table_name='ai_interactions') + op.drop_table('ai_interactions') + + # Drop types + # Note: Be careful with dropping types if they are used elsewhere, but here they are specific to this table. + sa.Enum(name='aiinteractiontype').drop(op.get_bind()) + sa.Enum(name='aiactiontype').drop(op.get_bind()) diff --git a/backend/requirements.txt b/backend/requirements.txt index 6c1bb2e..95788c8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -51,7 +51,7 @@ pydantic_core==2.41.5 Pygments==2.19.2 pyiceberg==0.10.0 PyJWT==2.11.0 -PyMuPDF==1.26.7 +pymupdf>=1.26.0 pyparsing==3.3.2 pypdf==6.7.0 pyroaring==1.0.3 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..207e1a8 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + env_file: + - ./backend/.env.dev + volumes: + - ./backend:/app + - /app/.venv + - /app/__pycache__ + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + - VITE_MODE=dev + env_file: + - ./frontend/.env.dev + volumes: + - ./frontend:/app + - /app/node_modules + - /app/dist diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7d900f9..3823b1c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,8 +12,12 @@ RUN npm install # Copy source code COPY . . +# Build arguments +ARG VITE_MODE=production +ENV VITE_MODE=$VITE_MODE + # Build the application -RUN npm run build +RUN npm run build -- --mode $VITE_MODE # Stage 2: Serve the application with Nginx FROM nginx:alpine