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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
backend/start.sh text eol=lf
54 changes: 54 additions & 0 deletions backend/alembic/versions/6e2d1d2b7c4a_add_ai_interactions.py
Original file line number Diff line number Diff line change
@@ -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())
1 change: 1 addition & 0 deletions backend/app/models/ai_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class AIActionType(enum.Enum):
analogy = "analogy"
example = "example"
expand_acronym = "expand_acronym"
answer_question = "answer_question"


class AIInteraction(Base):
Expand Down
12 changes: 12 additions & 0 deletions backend/app/prompts/highlight/answer_question.md
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/study-room/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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 },
{ key: 'define', label: 'Define', icon: TextQuote },
{ 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'];
1 change: 1 addition & 0 deletions frontend/src/services/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const ACTION_MAP: Record<string, string> = {
example: 'example',
analogy: 'analogy',
acronym: 'expand_acronym',
question: 'answer_question',
};

/**
Expand Down
Loading