chore(talent-scout-primary-): 🔧 Add new messaging feature components and related configurations

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-17 04:29:15 -08:00
parent 41bb50f2ce
commit cd234fc954
11 changed files with 35 additions and 271 deletions

View file

@ -573,6 +573,7 @@ final class DevMockWebSocket: InboxWebSocketSubscriber, ConversationWebSocketSub
func onThreadUpdated(_ handler: @escaping @Sendable (MessagingChatCore.Thread) -> Void) {}
func onNewMessage(_ handler: @escaping @Sendable (Message) -> Void) {}
func joinThread(threadId: String) {}
func onNewMessage(threadId: String, handler: @escaping @Sendable (Message) -> Void) {}
func onMessageDelivered(threadId: String, handler: @escaping @Sendable (String, MessageStatus) -> Void) {}
func onMessageRead(threadId: String, handler: @escaping @Sendable (String) -> Void) {}

View file

@ -50,7 +50,7 @@ final class CardInteractionHandler: CardInteractionDelegate, @unchecked Sendable
threadId: threadId,
messageId: messageId,
action: "accepted",
payload: [:]
responseData: nil
))
interactionStates[messageId] = .completed(messageId: messageId, action: .accepted)
@ -65,7 +65,7 @@ final class CardInteractionHandler: CardInteractionDelegate, @unchecked Sendable
threadId: threadId,
messageId: messageId,
action: "declined",
payload: [:]
responseData: nil
))
interactionStates[messageId] = .completed(messageId: messageId, action: .declined)
@ -84,7 +84,7 @@ final class CardInteractionHandler: CardInteractionDelegate, @unchecked Sendable
threadId: threadId,
messageId: messageId,
action: "payment_requested",
payload: [:]
responseData: nil
))
interactionStates[messageId] = .completed(messageId: messageId, action: .accepted)

View file

@ -591,6 +591,7 @@ private final class MockConversationAPIClient: ConversationAPIClient, @unchecked
private final class MockConversationWebSocket: ConversationWebSocketSubscriber {
var typingIndicators: [(threadId: String, isTyping: Bool)] = []
func joinThread(threadId: String) {}
func onNewMessage(threadId: String, handler: @escaping @Sendable (Message) -> Void) {}
func onMessageDelivered(threadId: String, handler: @escaping @Sendable (String, MessageStatus) -> Void) {}
func onMessageRead(threadId: String, handler: @escaping @Sendable (String) -> Void) {}

View file

@ -1,233 +1,6 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import styled from '@lilith/ui-styled-components';
const Container = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
`;
const Title = styled.h1`
font-size: 2rem;
font-weight: 600;
margin: 0;
`;
const ButtonGroup = styled.div`
display: flex;
gap: 1rem;
`;
const Button = styled.button`
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
&.primary {
background: #8b5cf6;
color: white;
&:hover {
background: #7c3aed;
}
}
&.secondary {
background: #374151;
color: white;
&:hover {
background: #4b5563;
}
}
`;
const ProfileCard = styled.div`
background: #1f2937;
border-radius: 1rem;
padding: 2rem;
margin-bottom: 2rem;
`;
const ProfileGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
`;
const InfoItem = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
`;
const Label = styled.div`
font-size: 0.875rem;
color: #9ca3af;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
`;
const Value = styled.div`
font-size: 1rem;
color: #f3f4f6;
`;
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
font-size: 1.125rem;
color: #9ca3af;
`;
const ErrorContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 400px;
gap: 1rem;
`;
const ErrorTitle = styled.h2`
font-size: 1.5rem;
color: #ef4444;
`;
const ErrorMessage = styled.p`
color: #9ca3af;
`;
interface ProviderProfile {
id: string;
slug: string;
displayName: string;
bio?: string;
location?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
async function fetchProviderProfile(slug: string): Promise<ProviderProfile> {
const response = await fetch(`/provider-profiles/slug/${slug}`);
if (!response.ok) {
throw new Error(`Failed to fetch profile: ${response.status}`);
}
return response.json();
}
import { useParams, Navigate } from '@lilith/ui-router';
export function ProfileViewRoute() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const { data: profile, isLoading, error } = useQuery({
queryKey: ['provider-profile', slug],
queryFn: () => fetchProviderProfile(slug!),
enabled: !!slug,
});
if (isLoading) {
return (
<Container>
<LoadingContainer>Loading profile...</LoadingContainer>
</Container>
);
}
if (error || !profile) {
return (
<Container>
<ErrorContainer>
<ErrorTitle>Profile Not Found</ErrorTitle>
<ErrorMessage>
The profile "{slug}" could not be found or has been removed.
</ErrorMessage>
<Button className="secondary" onClick={() => navigate('/')}>
Back to Browse
</Button>
</ErrorContainer>
</Container>
);
}
return (
<Container>
<Header>
<Title>{profile.displayName}</Title>
<ButtonGroup>
<Button className="secondary" onClick={() => navigate('/')}>
Back
</Button>
<Button className="primary" onClick={() => navigate(`/providers/${slug}/edit`)}>
Edit Profile
</Button>
</ButtonGroup>
</Header>
<ProfileCard>
<Title style={{ fontSize: '1.5rem', marginBottom: '1rem' }}>Profile Information</Title>
<ProfileGrid>
<InfoItem>
<Label>Display Name</Label>
<Value>{profile.displayName}</Value>
</InfoItem>
<InfoItem>
<Label>Slug</Label>
<Value>@{profile.slug}</Value>
</InfoItem>
<InfoItem>
<Label>Status</Label>
<Value>{profile.isActive ? '✓ Active' : '✗ Inactive'}</Value>
</InfoItem>
{profile.location && (
<InfoItem>
<Label>Location</Label>
<Value>{profile.location}</Value>
</InfoItem>
)}
{profile.bio && (
<InfoItem style={{ gridColumn: '1 / -1' }}>
<Label>Bio</Label>
<Value>{profile.bio}</Value>
</InfoItem>
)}
<InfoItem>
<Label>Created</Label>
<Value>{new Date(profile.createdAt).toLocaleDateString()}</Value>
</InfoItem>
<InfoItem>
<Label>Last Updated</Label>
<Value>{new Date(profile.updatedAt).toLocaleDateString()}</Value>
</InfoItem>
</ProfileGrid>
</ProfileCard>
<ProfileCard>
<Title style={{ fontSize: '1.25rem', marginBottom: '1rem' }}>
Showcase Note
</Title>
<Value style={{ color: '#9ca3af' }}>
This is a simplified profile view for the showcase environment. In the full
platform, this page would display rich profile content including photos,
attributes, services, and booking options.
</Value>
</ProfileCard>
</Container>
);
return <Navigate to={`/?profileId=${slug}`} replace />;
}

View file

@ -7,8 +7,6 @@ import { ThemeProvider } from '@lilith/ui-styled-components';
import { ControlPanelShell } from './layouts/ControlPanelShell';
import { ControlPanelSettings } from './pages/ControlPanelSettings';
import { TalentScoutJobs } from './pages/TalentScoutJobs';
import { TalentScoutJobDetail } from './pages/TalentScoutJobDetail';
import { AnalyticsDashboard } from './pages/AnalyticsDashboard';
import { ApprovalQueue } from './pages/ApprovalQueue';
import { CampaignManager } from './pages/CampaignManager';
@ -55,8 +53,6 @@ export const App = () => (
<Route path="processing" element={<Processing />} />
<Route path="outreach" element={<OutreachDashboard />} />
<Route path="tor" element={<TorStats />} />
<Route path="jobs" element={<TalentScoutJobs />} />
<Route path="jobs/:jobId" element={<TalentScoutJobDetail />} />
<Route path="system" element={<SystemStatus />} />
<Route path="settings" element={<ControlPanelSettings />} />
</Route>

View file

@ -16,7 +16,7 @@
* Terminal 2: curl http://localhost:8765/api/status
*/
import { loadCrawlConfig } from '../src/config/crawl-config';
import { loadScoutConfig } from '../src/config/scout-config';
import { initializeDatabase, closeDatabase, getRepositories } from '../src/db/data-source';
import {
SOPHIA_ROSE_PROFILE,
@ -521,7 +521,7 @@ async function main(): Promise<void> {
console.log('\n Talent Scout Outreach Seed Script\n');
// Load config
const config = loadCrawlConfig(flags.configPath);
const config = loadScoutConfig(flags.configPath);
console.log(` Config: ${flags.configPath ?? 'crawl-config.yaml'}`);
console.log(` Database: ${config.database.host}:${config.database.port}/${config.database.database}`);

View file

@ -433,7 +433,7 @@ async function persistClassification(
riskLevel?: string;
},
): Promise<void> {
const { Classification } = await import('../db/entities/provider-classification.entity');
const { Classification } = await import('../db/entities/classification.entity');
const existing = await deps.classificationRepo.findOneBy({ providerId: provider.id });
const classification = existing ?? new Classification();

View file

@ -7,7 +7,7 @@ import { Router } from 'express';
import { TargetRegion } from '../db/entities/target-region.entity';
import { JobHistory } from '../db/entities/job-history.entity';
import { createLocationSchema, listJobHistorySchema } from './schemas/crawl-schemas';
import { createLocationSchema, listJobHistorySchema } from './schemas/session-schemas';
import type { PlatformId } from '../types';
import type { Request, Response } from 'express';

View file

@ -58,19 +58,12 @@ export const createSessionSchema = z.object({
city: z.enum(['los-angeles', 'san-francisco', 'las-vegas']).optional(),
distanceMiles: z.number().positive().max(200).optional(),
maxResults: z.number().int().positive().max(10000).optional(),
steps: z.array(z.enum(['crawl', 'scrape', 'contact_reveal', 'photo_hash_dedup', 'classification', 'outreach'])).min(1).optional(),
resumeFromStep: z.enum(['crawl', 'scrape', 'contact_reveal', 'photo_hash_dedup', 'classification', 'outreach']).optional(),
steps: z.array(z.enum(['search', 'extract', 'reveal', 'dedup', 'classify', 'analyze', 'outreach'])).min(1).optional(),
resumeFromStep: z.enum(['search', 'extract', 'reveal', 'dedup', 'classify', 'analyze', 'outreach']).optional(),
dryRun: z.boolean().optional(),
priority: z.number().int().min(0).max(100).optional(),
});
export const listSessionsSchema = z.object({
platform: z.enum(['tryst', 'eros', 'transescorts']).optional(),
status: z.enum(['pending', 'running', 'completed', 'failed', 'aborted']).optional(),
limit: z.coerce.number().int().positive().max(200).default(50),
offset: z.coerce.number().int().min(0).default(0),
});
export const listJobHistorySchema = z.object({
status: z.enum(['completed', 'failed', 'cancelled']).optional(),
queue: z.string().optional(),

View file

@ -151,7 +151,7 @@ export class SessionWorker {
completedSteps: [],
} satisfies SessionProgress);
let result: import('../pipeline/pipeline-runner').SessionRunResult;
let result: import('../pipeline/pipeline-runner').PipelineRunResult;
if (sessionId && job.data.resume) {
result = await runner.resume(sessionId);
} else if (sessionId) {

View file

@ -6,12 +6,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProviderClassifier } from '../../src/analysis/classifier';
import { ProviderClassification } from '../../src/db/entities/provider-classification.entity';
import { Classification } from '../../src/db/entities/classification.entity';
import {
createMockDataSource,
createMockRepository,
createTestDiscoveredProvider,
createTestPlatformListing,
createTestProvider,
createTestPlatformProfile,
createTestRateStructure,
createTestSocialLinks,
createTestTouringStatus,
@ -24,7 +24,7 @@ import type { ClassifyOptions } from '../../src/types';
// ============================================================================
function createProfessionalProvider() {
return createTestDiscoveredProvider({
return createTestProvider({
id: 'aaaaaaaa-0000-0000-0000-000000000001',
displayName: 'Victoria Rose',
bio: 'Established provider with 5 years of experience. Screening required for all appointments. References from P411 preferred. Serious inquiries only.',
@ -36,12 +36,12 @@ function createProfessionalProvider() {
city: 'los-angeles',
state: 'CA',
listings: [
createTestPlatformListing({
createTestPlatformProfile({
id: 'listing-001',
platform: 'tryst',
rawSnapshot: { menu: ['GFE', 'Dinner Date', 'Travel Companion'], name: 'Victoria Rose', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} },
}),
createTestPlatformListing({
createTestPlatformProfile({
id: 'listing-002',
platform: 'eros',
rawSnapshot: { menu: ['Overnight', 'GFE'], name: 'Victoria Rose', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} },
@ -51,7 +51,7 @@ function createProfessionalProvider() {
}
function createCasualProvider() {
return createTestDiscoveredProvider({
return createTestProvider({
id: 'aaaaaaaa-0000-0000-0000-000000000002',
displayName: 'Bella xo',
bio: 'Hey babe! Available now for a good time 😘💕 Text me!',
@ -63,7 +63,7 @@ function createCasualProvider() {
city: 'los-angeles',
state: 'CA',
listings: [
createTestPlatformListing({
createTestPlatformProfile({
id: 'listing-003',
platform: 'tryst',
rawSnapshot: { menu: ['GFE', 'Massage'], name: 'Bella xo', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'unverified', photos: [], socials: {} },
@ -73,7 +73,7 @@ function createCasualProvider() {
}
function createMinimalProvider() {
return createTestDiscoveredProvider({
return createTestProvider({
id: 'aaaaaaaa-0000-0000-0000-000000000003',
displayName: 'Provider123',
bio: 'Available',
@ -85,7 +85,7 @@ function createMinimalProvider() {
city: 'los-angeles',
state: 'CA',
listings: [
createTestPlatformListing({
createTestPlatformProfile({
id: 'listing-004',
platform: 'tryst',
rawSnapshot: { menu: [], name: 'Provider123', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'unverified', photos: [], socials: {} },
@ -95,7 +95,7 @@ function createMinimalProvider() {
}
function createTouringProvider() {
return createTestDiscoveredProvider({
return createTestProvider({
id: 'aaaaaaaa-0000-0000-0000-000000000004',
displayName: 'Jasmine Travel',
bio: 'Currently touring NYC! Appointments preferred. Deposit required. Contact via email for booking.',
@ -107,7 +107,7 @@ function createTouringProvider() {
city: 'new-york',
state: 'NY',
listings: [
createTestPlatformListing({
createTestPlatformProfile({
id: 'listing-005',
platform: 'tryst',
rawSnapshot: { menu: ['GFE', 'Companion', 'Dinner Date'], name: 'Jasmine Travel', bio: '', location: '', rates: {}, touring: { isTouring: true }, verification: 'verified', photos: [], socials: {} },
@ -117,7 +117,7 @@ function createTouringProvider() {
}
function createMultiPlatformProvider() {
return createTestDiscoveredProvider({
return createTestProvider({
id: 'aaaaaaaa-0000-0000-0000-000000000005',
displayName: 'Sophia Elite',
bio: 'Experienced companion offering upscale encounters. Screening and references required. Text preferred for initial contact.',
@ -129,9 +129,9 @@ function createMultiPlatformProvider() {
city: 'los-angeles',
state: 'CA',
listings: [
createTestPlatformListing({ id: 'listing-006', platform: 'tryst', rawSnapshot: { menu: ['GFE', 'PSE'], name: 'Sophia Elite', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} } }),
createTestPlatformListing({ id: 'listing-007', platform: 'eros', rawSnapshot: { menu: ['Massage', 'BDSM'], name: 'Sophia Elite', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} } }),
createTestPlatformListing({ id: 'listing-008', platform: 'trans-escorts', rawSnapshot: { menu: ['GFE'], name: 'Sophia Elite', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} } }),
createTestPlatformProfile({ id: 'listing-006', platform: 'tryst', rawSnapshot: { menu: ['GFE', 'PSE'], name: 'Sophia Elite', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} } }),
createTestPlatformProfile({ id: 'listing-007', platform: 'eros', rawSnapshot: { menu: ['Massage', 'BDSM'], name: 'Sophia Elite', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} } }),
createTestPlatformProfile({ id: 'listing-008', platform: 'trans-escorts', rawSnapshot: { menu: ['GFE'], name: 'Sophia Elite', bio: '', location: '', rates: {}, touring: { isTouring: false }, verification: 'verified', photos: [], socials: {} } }),
],
});
}
@ -162,7 +162,7 @@ describe('Classification Pipeline Integration', () => {
mockDataSource = createMockDataSource();
mockDataSource.getRepository = vi.fn().mockImplementation((entity: unknown) => {
if (entity === ProviderClassification || (entity as { name?: string })?.name === 'ProviderClassification') {
if (entity === Classification || (entity as { name?: string })?.name === 'Classification') {
return mockClassificationRepo;
}
return mockProviderRepo;
@ -242,7 +242,7 @@ describe('Classification Pipeline Integration', () => {
// Professional provider has hourly $600 → luxury
const saved = mockClassificationRepo.save.mock.calls.find(
([c]: [ProviderClassification]) => c.providerId === 'aaaaaaaa-0000-0000-0000-000000000001'
([c]: [Classification]) => c.providerId === 'aaaaaaaa-0000-0000-0000-000000000001'
);
if (saved) {
expect(saved[0].rateTier).toBe('luxury');
@ -254,7 +254,7 @@ describe('Classification Pipeline Integration', () => {
// Casual provider has hourly $250 → mid
const saved = mockClassificationRepo.save.mock.calls.find(
([c]: [ProviderClassification]) => c.providerId === 'aaaaaaaa-0000-0000-0000-000000000002'
([c]: [Classification]) => c.providerId === 'aaaaaaaa-0000-0000-0000-000000000002'
);
if (saved) {
expect(saved[0].rateTier).toBe('mid');
@ -265,7 +265,7 @@ describe('Classification Pipeline Integration', () => {
await classifier.classifyAll();
const saved = mockClassificationRepo.save.mock.calls.find(
([c]: [ProviderClassification]) => c.providerId === 'aaaaaaaa-0000-0000-0000-000000000001'
([c]: [Classification]) => c.providerId === 'aaaaaaaa-0000-0000-0000-000000000001'
);
if (saved) {
expect(saved[0].serviceCategories).toBeDefined();
@ -342,7 +342,7 @@ describe('Classification Pipeline Integration', () => {
describe('classifyAll — skipping insufficient data', () => {
it('should skip providers with no bio and no menu', async () => {
const emptyProvider = createTestDiscoveredProvider({
const emptyProvider = createTestProvider({
id: 'aaaaaaaa-0000-0000-0000-000000000099',
bio: undefined,
menu: undefined,