Skip to content

Commit 6bf3d8b

Browse files
authored
Merge pull request #17 from BrainDriveAI/feature/conversation-page-id
Add `page_id` Support to Conversations and Page Context Awareness
2 parents ff80a2f + 3ff28b8 commit 6bf3d8b

File tree

10 files changed

+209
-3
lines changed

10 files changed

+209
-3
lines changed

backend/app/api/v1/endpoints/ai_providers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ async def chat_completion(request: ChatCompletionRequest, db: AsyncSession = Dep
497497
user_id=user_id,
498498
title=f"Conversation with {request.model}",
499499
page_context=request.page_context, # This is already defined in the schema with a default of None
500+
page_id=request.page_id, # NEW FIELD - ID of the page this conversation belongs to
500501
model=request.model,
501502
server=provider_instance.server_name,
502503
conversation_type=request.conversation_type or "chat" # New field with default

backend/app/api/v1/endpoints/conversations.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ async def get_user_conversations(
3030
limit: int = Query(20, ge=1, le=100),
3131
tag_id: Optional[str] = Query(None, description="Filter by tag ID"),
3232
conversation_type: Optional[str] = Query(None, description="Filter by conversation type"),
33+
page_id: Optional[str] = Query(None, description="Filter by page ID"),
3334
db: AsyncSession = Depends(get_db),
3435
current_user = Depends(get_current_user)
3536
):
@@ -48,6 +49,10 @@ async def get_user_conversations(
4849

4950
query = select(Conversation).where(Conversation.user_id == formatted_user_id)
5051

52+
# Filter by page_id if provided
53+
if page_id:
54+
query = query.where(Conversation.page_id == page_id)
55+
5156
# Filter by tag if provided
5257
if tag_id:
5358
query = query.join(
@@ -97,6 +102,7 @@ async def create_conversation(
97102
user_id=conversation_user_id, # Use formatted user ID
98103
title=conversation.title,
99104
page_context=conversation.page_context,
105+
page_id=conversation.page_id, # NEW FIELD
100106
model=conversation.model,
101107
server=conversation.server,
102108
conversation_type=conversation.conversation_type or "chat" # New field with default
@@ -161,6 +167,8 @@ async def update_conversation(
161167
conversation.title = conversation_update.title
162168
if conversation_update.page_context is not None:
163169
conversation.page_context = conversation_update.page_context
170+
if conversation_update.page_id is not None:
171+
conversation.page_id = conversation_update.page_id
164172
if conversation_update.model is not None:
165173
conversation.model = conversation_update.model
166174
if conversation_update.server is not None:

backend/app/models/conversation.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import uuid
22
from datetime import datetime
3-
from sqlalchemy import Column, String, Text, ForeignKey, DateTime, select
3+
from sqlalchemy import Column, String, Text, ForeignKey, DateTime, select, func
44
from sqlalchemy.dialects.postgresql import JSON
55
# Remove PostgreSQL UUID import as we're standardizing on String
66
from sqlalchemy.orm import relationship
@@ -14,6 +14,7 @@ class Conversation(Base, TimestampMixin):
1414
user_id = Column(String, ForeignKey("users.id"), nullable=False)
1515
title = Column(String)
1616
page_context = Column(String) # e.g., 'home', 'editor', 'chatbot_lab'
17+
page_id = Column(String(32), nullable=True) # NEW - specific page ID for page-specific conversations
1718
model = Column(String) # Store which model was used
1819
server = Column(String) # Store which server was used
1920
conversation_type = Column(String(100), nullable=True, default="chat") # New field for categorization
@@ -104,6 +105,36 @@ async def get_by_user_id(cls, db, user_id, skip=0, limit=100):
104105
result = await db.execute(query)
105106
return result.scalars().all()
106107

108+
@classmethod
109+
async def get_by_user_id_and_page(cls, db, user_id, page_id=None, conversation_type=None, skip=0, limit=100):
110+
"""Get conversations for user, optionally filtered by page_id and conversation_type."""
111+
formatted_user_id = str(user_id).replace('-', '')
112+
query = select(cls).where(cls.user_id == formatted_user_id)
113+
114+
# Filter by page_id if provided
115+
if page_id:
116+
query = query.where(cls.page_id == page_id)
117+
118+
# Filter by conversation_type if provided
119+
if conversation_type:
120+
query = query.where(cls.conversation_type == conversation_type)
121+
122+
query = query.order_by(cls.updated_at.desc()).offset(skip).limit(limit)
123+
result = await db.execute(query)
124+
return result.scalars().all()
125+
126+
@classmethod
127+
async def count_by_user_and_page(cls, db, user_id, page_id=None):
128+
"""Count conversations for user, optionally filtered by page_id."""
129+
formatted_user_id = str(user_id).replace('-', '')
130+
query = select(func.count(cls.id)).where(cls.user_id == formatted_user_id)
131+
132+
if page_id:
133+
query = query.where(cls.page_id == page_id)
134+
135+
result = await db.execute(query)
136+
return result.scalar()
137+
107138
async def get_messages(self, db, skip=0, limit=100):
108139
"""Get all messages for this conversation with pagination."""
109140
from app.models.message import Message

backend/app/schemas/ai_providers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class TextGenerationRequest(BaseModel):
2222
user_id: Optional[str] = Field(None, description="User ID for access control")
2323
params: Dict[str, Any] = Field(default_factory=dict, description="Additional parameters")
2424
stream: bool = Field(False, description="Whether to stream the response")
25+
page_id: Optional[str] = Field(None, description="ID of the page this generation belongs to")
26+
page_context: Optional[str] = Field(None, description="Context of the page where this generation is happening")
27+
conversation_type: Optional[str] = Field("chat", description="Type/category of the conversation")
2528

2629

2730
class ChatMessage(BaseModel):
@@ -41,6 +44,7 @@ class ChatCompletionRequest(BaseModel):
4144
params: Dict[str, Any] = Field(default_factory=dict, description="Additional parameters")
4245
stream: bool = Field(False, description="Whether to stream the response")
4346
conversation_id: Optional[str] = Field(None, description="ID of an existing conversation to continue")
47+
page_id: Optional[str] = Field(None, description="ID of the page this conversation belongs to")
4448
page_context: Optional[str] = Field(None, description="Context where the conversation is taking place (e.g., 'home', 'editor', 'chatbot_lab')")
4549
conversation_type: Optional[str] = Field("chat", description="Type/category of the conversation (e.g., 'chat', 'email_reply', 'therapy')")
4650

backend/app/schemas/conversation_schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Message(MessageInDB):
3333
class ConversationBase(BaseModel):
3434
title: Optional[str] = Field(None, description="Title of the conversation")
3535
page_context: Optional[str] = Field(None, description="Context where the conversation was created")
36+
page_id: Optional[str] = Field(None, description="ID of the page this conversation belongs to")
3637
model: Optional[str] = Field(None, description="LLM model used for the conversation")
3738
server: Optional[str] = Field(None, description="Server used for the conversation")
3839
conversation_type: Optional[str] = Field("chat", description="Type/category of the conversation (e.g., 'chat', 'email_reply', 'therapy')")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""add page_id to conversations
2+
3+
Revision ID: add_page_id_to_conversations
4+
Revises: add_conversation_type_simple
5+
Create Date: 2025-06-25 13:00:00.000000
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = 'add_page_id_to_conversations'
15+
down_revision: Union[str, None] = 'add_conversation_type_simple'
16+
branch_labels: Union[str, Sequence[str], None] = None
17+
depends_on: Union[str, Sequence[str], None] = None
18+
19+
20+
def upgrade() -> None:
21+
# Add page_id column to conversations table
22+
# Using batch_alter_table for SQLite compatibility
23+
with op.batch_alter_table('conversations', schema=None) as batch_op:
24+
batch_op.add_column(sa.Column('page_id', sa.String(length=32), nullable=True))
25+
26+
# Add indexes for efficient querying (SQLite compatible)
27+
op.create_index('idx_conversations_page_id', 'conversations', ['page_id'], unique=False)
28+
op.create_index('idx_conversations_user_page', 'conversations', ['user_id', 'page_id'], unique=False)
29+
30+
# Note: Existing conversations will have page_id = NULL (treated as global conversations)
31+
# New page-specific conversations will have specific page_id values
32+
33+
34+
def downgrade() -> None:
35+
# Remove indexes first
36+
op.drop_index('idx_conversations_user_page', table_name='conversations')
37+
op.drop_index('idx_conversations_page_id', table_name='conversations')
38+
39+
# Remove page_id column using batch_alter_table for SQLite compatibility
40+
with op.batch_alter_table('conversations', schema=None) as batch_op:
41+
batch_op.drop_column('page_id')
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""merge heads
2+
3+
Revision ID: c7eed881f009
4+
Revises: restore_settings_tables, add_page_id_to_conversations
5+
Create Date: 2025-06-25 13:34:43.475716
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = 'c7eed881f009'
16+
down_revision: Union[str, None] = ('restore_settings_tables', 'add_page_id_to_conversations')
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
pass
23+
24+
25+
def downgrade() -> None:
26+
pass

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SettingsService } from './services/SettingsService';
1010
import { UserSettingsInitService } from './services/UserSettingsInitService';
1111
import { userNavigationInitService } from './services/UserNavigationInitService';
1212
import { eventService } from './services/EventService';
13+
import { pageContextService } from './services/PageContextService';
1314
import { useAppTheme } from './hooks/useAppTheme';
1415
import { config } from './config';
1516
import { PluginManager } from './components/PluginManager';
@@ -38,6 +39,7 @@ serviceRegistry.registerService(settingsService);
3839
serviceRegistry.registerService(userSettingsInitService);
3940
serviceRegistry.registerService(userNavigationInitService);
4041
serviceRegistry.registerService(eventService);
42+
serviceRegistry.registerService(pageContextService);
4143

4244
function AppContent() {
4345
const theme = useAppTheme();

frontend/src/components/DynamicPageRenderer.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { defaultPageService } from '../services/defaultPageService';
1919
import { PluginModuleRenderer } from './PluginModuleRenderer';
2020
import { PluginProvider } from '../contexts/PluginContext';
2121
import { getAvailablePlugins } from '../plugins';
22+
import { pageContextService } from '../services/PageContextService';
2223

2324
interface DynamicPageRendererProps {
2425
pageId?: string; // Optional: directly specify page ID
@@ -124,21 +125,32 @@ export const DynamicPageRenderer: React.FC<DynamicPageRendererProps> = ({ pageId
124125
// Memoize the current page ID to prevent unnecessary recalculations
125126
const currentId = useMemo(() => getPageId(), [pageId, route, location.pathname]);
126127

127-
// Update global variables when page or studio status changes
128+
// Update global variables and page context when page or studio status changes
128129
useEffect(() => {
129130
if (page) {
130131
window.currentPageTitle = page.name;
131132
window.isStudioPage = isStudioPage;
132133

134+
// Update the page context service
135+
const pageContextData = {
136+
pageId: page.id || currentId,
137+
pageName: page.name || 'Unknown Page',
138+
pageRoute: page.route || location.pathname,
139+
isStudioPage
140+
};
141+
142+
pageContextService.setPageContext(pageContextData);
143+
133144
console.log('DynamicPageRenderer - Current path:', location.pathname);
134145
console.log('DynamicPageRenderer - Is studio page:', isStudioPage);
135146
console.log('DynamicPageRenderer - Page name:', page.name);
147+
console.log('DynamicPageRenderer - Page context updated:', pageContextData);
136148
console.log('DynamicPageRenderer - Global variables:', {
137149
currentPageTitle: window.currentPageTitle,
138150
isStudioPage: window.isStudioPage
139151
});
140152
}
141-
}, [page, isStudioPage, location.pathname]);
153+
}, [page, currentId, isStudioPage, location.pathname]);
142154

143155
// Function to handle module state changes
144156
const handleModuleStateChange = useCallback((moduleId: string, state: any) => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { AbstractBaseService } from './base/BaseService';
2+
3+
export interface PageContextData {
4+
pageId: string;
5+
pageName: string;
6+
pageRoute: string;
7+
isStudioPage: boolean;
8+
}
9+
10+
export interface PageContextServiceInterface {
11+
getCurrentPageContext(): PageContextData | null;
12+
onPageContextChange(callback: (context: PageContextData) => void): () => void;
13+
}
14+
15+
class PageContextServiceImpl extends AbstractBaseService implements PageContextServiceInterface {
16+
private currentContext: PageContextData | null = null;
17+
private listeners: ((context: PageContextData) => void)[] = [];
18+
private static instance: PageContextServiceImpl;
19+
20+
private constructor() {
21+
super(
22+
'pageContext',
23+
{ major: 1, minor: 0, patch: 0 },
24+
[
25+
{
26+
name: 'page-context-management',
27+
description: 'Page context tracking and management capabilities',
28+
version: '1.0.0'
29+
},
30+
{
31+
name: 'page-context-events',
32+
description: 'Page context change event subscription system',
33+
version: '1.0.0'
34+
}
35+
]
36+
);
37+
}
38+
39+
public static getInstance(): PageContextServiceImpl {
40+
if (!PageContextServiceImpl.instance) {
41+
PageContextServiceImpl.instance = new PageContextServiceImpl();
42+
}
43+
return PageContextServiceImpl.instance;
44+
}
45+
46+
async initialize(): Promise<void> {
47+
// Initialize the service - no special initialization needed for now
48+
console.log('[PageContextService] Initialized');
49+
}
50+
51+
async destroy(): Promise<void> {
52+
// Clean up listeners
53+
this.listeners = [];
54+
this.currentContext = null;
55+
console.log('[PageContextService] Destroyed');
56+
}
57+
58+
getCurrentPageContext(): PageContextData | null {
59+
return this.currentContext;
60+
}
61+
62+
setPageContext(context: PageContextData): void {
63+
this.currentContext = context;
64+
this.listeners.forEach(listener => listener(context));
65+
}
66+
67+
onPageContextChange(callback: (context: PageContextData) => void): () => void {
68+
this.listeners.push(callback);
69+
70+
// Return unsubscribe function
71+
return () => {
72+
const index = this.listeners.indexOf(callback);
73+
if (index > -1) {
74+
this.listeners.splice(index, 1);
75+
}
76+
};
77+
}
78+
}
79+
80+
export const pageContextService = PageContextServiceImpl.getInstance();

0 commit comments

Comments
 (0)