Skip to content
Open
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
11 changes: 11 additions & 0 deletions MtdrSpring/backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-generativeaiinference</artifactId>
<version>3.46.1</version>
</dependency>
<dependency>
<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-common-httpclient-jersey3</artifactId>
<version>3.46.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
163 changes: 163 additions & 0 deletions MtdrSpring/backend/src/main/frontend/src/AIInsightsPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* AI Insights chat panel — RAG-backed Q&A over project data.
* Calls POST /api/ai/ask and renders answer + source chips.
*/
import React, { useState } from 'react';
import {
Alert,
Box,
Button,
Card,
CardContent,
Chip,
CircularProgress,
Snackbar,
Stack,
TextField,
Typography
} from '@mui/material';
import { API_AI } from './API';

const SUGGESTIONS = [
'¿Cuál es el sprint activo?',
'¿Quién tiene más tareas BLOCKED?',
'Resumen de KPIs del proyecto',
'¿Cuántos incidentes CRITICAL hay este mes?'
];

function AIInsightsPanel() {
const [input, setInput] = useState('');
const [messages, setMessages] = useState([]); // {role: 'user'|'assistant', content, sources?}
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const send = (q) => {
const question = (q !== undefined ? q : input).trim();
if (!question || loading) return;
setMessages((m) => [...m, { role: 'user', content: question }]);
setInput('');
setLoading(true);
fetch(API_AI + '/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
})
.then((r) => r.json().then((body) => ({ ok: r.ok, body })))
.then(({ ok, body }) => {
setLoading(false);
if (!ok) {
setError(body.error || 'Error desconocido');
return;
}
setMessages((m) => [
...m,
{ role: 'assistant', content: body.answer, sources: body.sources || [] }
]);
})
.catch((err) => {
setLoading(false);
setError(err.message || 'Network error');
});
};

const onKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
};

return (
<Card variant="outlined" sx={{ mb: 4 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Preguntale al proyecto
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Haz preguntas en lenguaje natural sobre sprints, tareas, miembros, KPIs, deploys e incidentes.
</Typography>

<Stack direction="row" spacing={1} sx={{ mb: 2, flexWrap: 'wrap', gap: 1 }}>
{SUGGESTIONS.map((s) => (
<Chip
key={s}
label={s}
size="small"
clickable
disabled={loading}
onClick={() => send(s)}
/>
))}
</Stack>

<Box sx={{ maxHeight: 400, overflowY: 'auto', mb: 2, p: 1, bgcolor: '#fafafa', borderRadius: 1 }}>
{messages.length === 0 && (
<Typography variant="body2" color="text.secondary">
No has hecho ninguna pregunta todavia.
</Typography>
)}
{messages.map((m, i) => (
<Box key={i} sx={{ mb: 2 }}>
<Typography variant="caption" color={m.role === 'user' ? 'primary' : 'secondary'}>
{m.role === 'user' ? 'Tu' : 'IA'}
</Typography>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
{m.content}
</Typography>
{m.sources && m.sources.length > 0 && (
<Stack direction="row" spacing={0.5} sx={{ mt: 0.5, flexWrap: 'wrap', gap: 0.5 }}>
{m.sources.map((s, j) => (
<Chip
key={j}
label={`${s.sourceType} #${s.sourceId}`}
size="small"
variant="outlined"
/>
))}
</Stack>
)}
</Box>
))}
{loading && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="caption">pensando...</Typography>
</Box>
)}
</Box>

<Stack direction="row" spacing={1}>
<TextField
fullWidth
size="small"
placeholder="Escribe tu pregunta..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={onKeyDown}
disabled={loading}
/>
<Button
variant="contained"
onClick={() => send()}
disabled={loading || !input.trim()}
>
Preguntar
</Button>
</Stack>
</CardContent>

<Snackbar
open={!!error}
autoHideDuration={6000}
onClose={() => setError(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert severity="error" onClose={() => setError(null)}>
{error}
</Alert>
</Snackbar>
</Card>
);
}

export default AIInsightsPanel;
1 change: 1 addition & 0 deletions MtdrSpring/backend/src/main/frontend/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
// Example: const API_LIST = 'https://di2eyonlz5s7kmuektcddaw5zq.apigateway.<region>.oci.customer-oci.com/todolist';
// const API_LIST = 'https://di2eyonlz5s7kmuektcddaw5zq.apigateway.eu-frankfurt-1.oci.customer-oci.com/todolist';
const API_LIST = '/todolist';
export const API_AI = '/api/ai';
export default API_LIST;
2 changes: 2 additions & 0 deletions MtdrSpring/backend/src/main/frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
import React, { useState, useEffect } from 'react';
import NewItem from './NewItem';
import AIInsightsPanel from './AIInsightsPanel';
import API_LIST from './API';
import DeleteIcon from '@mui/icons-material/Delete';
import { Button, TableBody, CircularProgress } from '@mui/material';
Expand Down Expand Up @@ -186,6 +187,7 @@ function App() {
return (
<div className="App">
<h1>MY TODO LIST</h1>
<AIInsightsPanel />
<NewItem addItem={addItem} isInserting={isInserting}/>
{ error &&
<p>Error: {error.message}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
package com.springboot.MyTodoList.config;

import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DeepSeekConfig {

@Value("${deepseek.api.key}")
private String apiKey;

@Bean
public CloseableHttpClient httpClient() {
return HttpClients.createDefault();
}

@Bean
public HttpPost deepSeekRequest(@Value("${deepseek.api.url}") String apiUrl) {
HttpPost request = new HttpPost(apiUrl);
request.addHeader("Content-Type", "application/json");
request.addHeader("Authorization", "Bearer " + apiKey);
return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.springboot.MyTodoList.config;

import com.oracle.bmc.Region;
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class OCIEmbeddingConfig {

private static final Logger LOG = LoggerFactory.getLogger(OCIEmbeddingConfig.class);

@Value("${oci.auth.mode:CONFIG_FILE}")
private String authMode;

@Value("${oci.auth.profile:DEFAULT}")
private String authProfile;

@Value("${oci.genai.region:us-chicago-1}")
private String regionId;

@Bean
public GenerativeAiInferenceClient generativeAiInferenceClient() throws IOException {
BasicAuthenticationDetailsProvider provider;
if ("INSTANCE_PRINCIPAL".equalsIgnoreCase(authMode)) {
LOG.info("OCI auth: Instance Principals");
provider = InstancePrincipalsAuthenticationDetailsProvider.builder().build();
} else {
LOG.info("OCI auth: ConfigFile profile={}", authProfile);
provider = new ConfigFileAuthenticationDetailsProvider(authProfile);
}
GenerativeAiInferenceClient client = GenerativeAiInferenceClient.builder()
.region(Region.fromRegionId(regionId))
.build(provider);
return client;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.springboot.MyTodoList.controller;

import com.springboot.MyTodoList.dto.AIQueryRequest;
import com.springboot.MyTodoList.dto.AIQueryResponse;
import com.springboot.MyTodoList.service.AIInsightsService;
import com.springboot.MyTodoList.service.rag.DocumentIngestionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.Map;

@RestController
@RequestMapping("/api/ai")
public class AIInsightsController {

private static final Logger LOG = LoggerFactory.getLogger(AIInsightsController.class);

private final AIInsightsService insights;
private final DocumentIngestionService ingestion;

@Autowired
public AIInsightsController(AIInsightsService insights, DocumentIngestionService ingestion) {
this.insights = insights;
this.ingestion = ingestion;
}

@PostMapping("/ask")
public ResponseEntity<?> ask(@RequestBody AIQueryRequest req) {
if (req == null || req.getQuestion() == null || req.getQuestion().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "question is required"));
}
try {
AIQueryResponse resp = insights.ask(req.getQuestion());
return ResponseEntity.ok(resp);
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
} catch (IOException ex) {
LOG.error("AI ask failed: {}", ex.getMessage(), ex);
return ResponseEntity.status(502).body(Map.of("error", "upstream LLM error: " + ex.getMessage()));
} catch (RuntimeException ex) {
LOG.error("AI ask failed: {}", ex.getMessage(), ex);
return ResponseEntity.status(500).body(Map.of("error", ex.getMessage()));
}
}

@PostMapping("/reindex")
public ResponseEntity<?> reindexAll() {
try {
Map<String, Integer> totals = ingestion.reindexAll();
return ResponseEntity.ok(Map.of("ok", true, "indexed", totals));
} catch (RuntimeException ex) {
LOG.error("Reindex failed: {}", ex.getMessage(), ex);
return ResponseEntity.status(500).body(Map.of("error", ex.getMessage()));
}
}

@PostMapping("/reindex/{sourceType}")
public ResponseEntity<?> reindexOne(@PathVariable String sourceType) {
try {
Map<String, Integer> totals = ingestion.reindex(sourceType);
return ResponseEntity.ok(Map.of("ok", true, "indexed", totals));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
} catch (RuntimeException ex) {
LOG.error("Reindex failed: {}", ex.getMessage(), ex);
return ResponseEntity.status(500).body(Map.of("error", ex.getMessage()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.springboot.MyTodoList.dto;

public class AIQueryRequest {
private String question;

public String getQuestion() { return question; }
public void setQuestion(String question) { this.question = question; }
}
Loading