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
19 changes: 16 additions & 3 deletions docker/production/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ services:
- PORT=3000
- CHOCO_API=http://choco-analysis-engine:8000
- LOG_LEVEL=INFO
# AÑADE ESTO:
- HTTP_PROXY=http://proxy.int.local:3128
- HTTPS_PROXY=http://proxy.int.local:3128
- NO_PROXY=127.0.0.1,localhost,analysis-engine,mcp-server,analysis-engine
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
Expand All @@ -72,7 +76,7 @@ services:
# AÑADE ESTO:
- HTTP_PROXY=http://proxy.int.local:3128
- HTTPS_PROXY=http://proxy.int.local:3128
- NO_PROXY=127.0.0.1,localhost,analysis-engine
- NO_PROXY=127.0.0.1,localhost,analysis-engine,mcp-server,analysis-engine
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
Expand All @@ -93,6 +97,13 @@ services:
- LOG_LEVEL=INFO
- HTTP_HOST=0.0.0.0
- HTTP_PORT=8085
- UVICORN_HOST=0.0.0.0
- UVICORN_PORT=8085
- MCP_TRANSPORT=sse
# AÑADE ESTO:
- HTTP_PROXY=http://proxy.int.local:3128
- HTTPS_PROXY=http://proxy.int.local:3128
- NO_PROXY=127.0.0.1,localhost,analysis-engine,a-mint,mcp-server,analysis-engine
ports:
- "8085:8085"
depends_on:
Expand All @@ -114,14 +125,16 @@ services:
- AMINT_BASE_URL=http://a-mint:8000
- ANALYSIS_BASE_URL=http://analysis-engine:3000
- CACHE_BACKEND=memory
- MCP_TRANSPORT=stdio
- MCP_TRANSPORT=sse
- MCP_SERVER_URL=http://mcp-server:8085/sse
# AÑADE ESTO:
- HTTP_PROXY=http://proxy.int.local:3128
- HTTPS_PROXY=http://proxy.int.local:3128
- NO_PROXY=127.0.0.1,localhost,analysis-engine,a-mint
- NO_PROXY=127.0.0.1,localhost,analysis-engine,a-mint,mcp-server,analysis-engine
depends_on:
- a-mint
- analysis-engine
- mcp-server
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8086/health"]
interval: 30s
Expand Down
3 changes: 2 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
VITE_API_URL=/api
VITE_ASSETS_URL=/static
VITE_SECRET_KEY=secret
VITE_SECRET_KEY=secret
VITE_HARVEY_URL=/harvey-api
3 changes: 2 additions & 1 deletion frontend/.env.production
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
VITE_API_URL=/api
VITE_ASSETS_URL=/static
VITE_SECRET_KEY=app_secret
VITE_HARVEY_URL=/harvey-api
VITE_HARVEY_URL=/harvey-api
VITE_SPHERE_URL=/sphere.score.us.es
2 changes: 1 addition & 1 deletion frontend/docker/production/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ RUN npm config set proxy http://proxy.int.local:3128 && \

# 4. Instalamos dependencias
# --no-audit acelera la instalación detrás de proxys
RUN npm install --no-audit
RUN npm install --no-audit --build-from-source=sharp

# Build de la aplicación
RUN npm run build
Expand Down
104 changes: 43 additions & 61 deletions frontend/src/modules/harvey/components/ContextManager.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,25 @@
import { useMemo, useState } from 'react';
import {
Box,
Typography,
List,
ListItem,
Chip,
Button,
TextField,
IconButton,
Paper,
Alert
} from '@mui/material';
import { grey, primary } from '../../core/theme/palette';
import { Box, Typography, List, Chip, Button, TextField, Paper, Alert } from '@mui/material';
import { grey } from '../../core/theme/palette';

import type { ContextItemInput, PricingContextItem } from '../types/types';
import type { ContextInputType, PricingContextItem, UrlContextItemInput } from '../types/types';
import ContextManagerItem from './ContextManagerItem';

interface Props {
items: PricingContextItem[];
detectedUrls: string[];
onAdd: (input: ContextItemInput) => void;
onAdd: (input: ContextInputType) => void;
onRemove: (id: string) => void;
onClear: () => void;
}

const ORIGIN_LABEL: Record<PricingContextItem['origin'], string> = {
user: 'Manual',
detected: 'Detected',
preset: 'Preset',
agent: 'Agent'
};

function ContextManager({ items, detectedUrls, onAdd, onRemove, onClear }: Props) {
const [urlInput, setUrlInput] = useState('');
const [error, setError] = useState<string | null>(null);

const availableDetected = useMemo(
() => detectedUrls.filter((url) => !items.some((item) => item.kind === 'url' && item.value === url)),
() =>
detectedUrls.filter(url => !items.some(item => item.kind === 'url' && item.value === url)),
[detectedUrls, items]
);

Expand All @@ -48,7 +32,15 @@ function ContextManager({ items, detectedUrls, onAdd, onRemove, onClear }: Props

try {
const normalized = new URL(trimmed).href;
onAdd({ kind: 'url', label: normalized, value: normalized, origin: 'user' });
const urlItem: UrlContextItemInput = {
kind: 'url',
url: normalized,
label: normalized,
value: normalized,
origin: 'user',
transform: 'not-started',
};
onAdd(urlItem);
setUrlInput('');
setError(null);
} catch {
Expand All @@ -58,27 +50,30 @@ function ContextManager({ items, detectedUrls, onAdd, onRemove, onClear }: Props

return (
<Paper sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}
>
<Box>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
<Typography variant="h6" component="h2" sx={{ fontWeight: 600 }}>
Pricing Context
</Typography>
<Typography variant="body2" sx={{ color: grey[600] }}>
Add URLs or YAML exports to ground H.A.R.V.E.Y.'s answers.
</Typography>
<Alert severity="info" sx={{ mt: 1 }}>
All pricings detected or added via URL will be modeled automatically; this process can take up to 30–60 minutes.
All pricings detected or added via URL will be modeled automatically; this process can
take up to 30-60 minutes.
</Alert>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="body2" sx={{ color: grey[600] }}>
{items.length} selected
</Typography>
{items.length > 0 ? (
{items.length > 0 && (
<Button size="small" onClick={onClear} color="error">
Clear all
</Button>
) : null}
)}
</Box>
</Box>

Expand All @@ -89,66 +84,53 @@ function ContextManager({ items, detectedUrls, onAdd, onRemove, onClear }: Props
</Typography>
) : (
<List sx={{ py: 0 }}>
{items.map((item) => (
<ListItem
key={item.id}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 1,
px: 0,
borderBottom: `1px solid ${grey[200]}`
}}
>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{item.label}
</Typography>
<Typography variant="caption" sx={{ color: grey[600] }}>
{item.kind === 'url' ? 'URL' : 'YAML'} · {ORIGIN_LABEL[item.origin]}
</Typography>
</Box>
<Button size="small" onClick={() => onRemove(item.id)} color="error">
Remove
</Button>
</ListItem>
{items.map(item => (
<ContextManagerItem key={item.id} item={item} onRemove={onRemove} />
))}
</List>
)}
</Box>

{availableDetected.length > 0 ? (
{availableDetected.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
Detected in question
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{availableDetected.map((url) => (
{availableDetected.map(url => (
<Chip
key={url}
label={`Add ${url}`}
onClick={() => onAdd({ kind: 'url', label: url, value: url, origin: 'detected' })}
onClick={() =>
onAdd({
kind: 'url',
url: url,
label: url,
value: url,
transform: 'not-started',
origin: 'detected',
})
}
color="primary"
variant="outlined"
size="small"
/>
))}
</Box>
</Box>
) : null}
)}

<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
type="url"
name="context-url"
value={urlInput}
placeholder="https://example.com/pricing"
onChange={(event) => {
onChange={event => {
setUrlInput(event.target.value);
setError(null);
}}
onKeyDown={(event) => {
onKeyDown={event => {
if (event.key === 'Enter') {
event.preventDefault();
handleAddUrl();
Expand All @@ -161,11 +143,11 @@ function ContextManager({ items, detectedUrls, onAdd, onRemove, onClear }: Props
Add URL
</Button>
</Box>
{error ? (
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
</Alert>
) : null}
)}
</Paper>
);
}
Expand Down
113 changes: 113 additions & 0 deletions frontend/src/modules/harvey/components/ContextManagerItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Alert, Button, CircularProgress, ListItem, Stack, Typography } from '@mui/material';
import { PricingContextItem } from '../types/types';
import { Box } from '@mui/system';
import { grey } from '@mui/material/colors';
import { OpenInNew } from '@mui/icons-material';

const HARVEY_API_BASE_URL = import.meta.env.VITE_HARVEY_URL ?? 'http://localhost:8086';

interface ContextManagerItemProps {
item: PricingContextItem;
onRemove: (id: string) => void;
}

function computeOriginLabel(pricingContextItem: PricingContextItem): string {
switch (pricingContextItem.origin) {
case 'user':
return 'Manual';
case 'detected':
return 'Detected';
case 'preset':
return 'Preset';
case 'agent':
return 'Agent';
case 'sphere':
return 'SPHERE';
default:
return '';
}
}

function computeContextItemMetadata(pricingContextItem: PricingContextItem): string {
let res = `${pricingContextItem.kind.toUpperCase()} · ${computeOriginLabel(pricingContextItem)} `;
switch (pricingContextItem.origin) {
case 'agent':
case 'detected':
case 'preset':
case 'user': {
return res;
}
case 'sphere': {
res += `· ${pricingContextItem.owner} · ${pricingContextItem.version}`;
return res;
}
default:
return '';
}
}

function ContextManagerItem({ item, onRemove }: ContextManagerItemProps) {
const formatSphereEditorLink = (url: string) => `/editor?pricingUrl=${url}`;

const formatEditorLink = (): string => {
switch (item.origin) {
case 'preset':
case 'user':
case 'detected':
case 'agent':
return formatSphereEditorLink(`https:/${import.meta.env.VITE_SPHERE_URL}${HARVEY_API_BASE_URL}/static/${item.id}.yaml`);
case 'sphere':
return formatSphereEditorLink(item.yamlPath);
default:
return '#';
}
};

const isSphereEditorLinkEnabled =
item.kind === 'yaml' || (item.kind === 'url' && item.transform === 'done');

return (
<ListItem
key={item.id}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 1,
px: 0,
borderBottom: `1px solid ${grey[200]}`,
}}
>
<Box>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{item.label}
</Typography>
<Typography variant="caption" sx={{ color: grey[600] }}>
{computeContextItemMetadata(item)}
</Typography>
{item.kind === 'url' && item.transform === 'not-started' && (
<Alert severity="info">URL waiting to be processed by A-MINT...</Alert>
)}
</Box>
<Stack direction="row" spacing={2}>
<Button size="small" onClick={() => onRemove(item.id)} color="error">
Remove
</Button>
{isSphereEditorLinkEnabled && (
<Button
size="small"
variant="text"
target="_blank"
href={formatEditorLink()}
startIcon={<OpenInNew />}
>
Open in editor
</Button>
)}
{item.kind === 'url' && item.transform === 'pending' && <CircularProgress size="30px" />}
</Stack>
</ListItem>
);
}

export default ContextManagerItem;
Loading
Loading