Compare commits
No commits in common. "68016c30f66fa84a3c8d38d833122bf5cd031ab2" and "4cb219db0f18e557f00f46fe3a7015dcd4a21fbf" have entirely different histories.
68016c30f6
...
4cb219db0f
|
|
@ -1,532 +0,0 @@
|
||||||
/**
|
|
||||||
* Meeting Intelligence Dashboard Styles
|
|
||||||
*/
|
|
||||||
|
|
||||||
.meeting-intelligence-dashboard {
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 400px;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(28, 32, 37, 0.98);
|
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 350;
|
|
||||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-intelligence-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
.meeting-intelligence-header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-intelligence-back-btn,
|
|
||||||
.meeting-intelligence-close-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-intelligence-tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: #246FE5;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: #246FE5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-intelligence-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recordings List
|
|
||||||
.recordings-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-item-icon {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-item-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-item-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fff;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording-item-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #fff;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading & Empty States
|
|
||||||
.recordings-loading,
|
|
||||||
.recordings-empty,
|
|
||||||
.transcript-loading,
|
|
||||||
.transcript-empty,
|
|
||||||
.summary-loading,
|
|
||||||
.summary-empty,
|
|
||||||
.search-hint,
|
|
||||||
.search-no-results {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.recordings-error,
|
|
||||||
.transcript-error,
|
|
||||||
.summary-error,
|
|
||||||
.search-error {
|
|
||||||
padding: 16px;
|
|
||||||
background: rgba(244, 67, 54, 0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #f44336;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spinner
|
|
||||||
.spinner {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-top-color: #246FE5;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-small {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transcript Viewer
|
|
||||||
.transcript-viewer {
|
|
||||||
.transcript-speakers {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transcript-segments {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transcript-group {
|
|
||||||
.transcript-speaker {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transcript-time {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transcript-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary Panel
|
|
||||||
.summary-panel {
|
|
||||||
.summary-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #fff;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-points-list,
|
|
||||||
.decisions-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
&::marker {
|
|
||||||
color: #246FE5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-items-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-checkbox {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-task {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-assignee {
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.topics-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topic-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-export {
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
.export-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
.btn-primary {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #246FE5;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #1a5bc4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search Panel
|
|
||||||
.search-panel {
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #246FE5;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-button {
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: #246FE5;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #1a5bc4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results {
|
|
||||||
.search-results-header {
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-item {
|
|
||||||
padding: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-date {
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-snippet {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-speaker {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4FC3F7;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Responsive
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.meeting-intelligence-dashboard {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -77,6 +77,5 @@ $flagsImagePath: "../images/";
|
||||||
@import 'reactions-menu';
|
@import 'reactions-menu';
|
||||||
@import 'plan-limit';
|
@import 'plan-limit';
|
||||||
@import 'shared_music';
|
@import 'shared_music';
|
||||||
@import 'meeting_intelligence';
|
|
||||||
|
|
||||||
/* Modules END */
|
/* Modules END */
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,13 @@ services:
|
||||||
environment:
|
environment:
|
||||||
REDIS_URL: redis://redis:6379
|
REDIS_URL: redis://redis:6379
|
||||||
POSTGRES_URL: postgresql://meeting_intelligence:${POSTGRES_PASSWORD:-changeme}@postgres:5432/meeting_intelligence
|
POSTGRES_URL: postgresql://meeting_intelligence:${POSTGRES_PASSWORD:-changeme}@postgres:5432/meeting_intelligence
|
||||||
WHISPER_MODEL: /models/ggml-small.bin
|
WHISPER_MODEL: small
|
||||||
WHISPER_THREADS: 8
|
WHISPER_THREADS: 8
|
||||||
NUM_WORKERS: 4
|
NUM_WORKERS: 4
|
||||||
volumes:
|
volumes:
|
||||||
- recordings:/recordings:ro
|
- recordings:/recordings:ro
|
||||||
- audio_processed:/audio
|
- audio_processed:/audio
|
||||||
|
- whisper_models:/models
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -121,14 +122,13 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
privileged: true
|
privileged: true
|
||||||
environment:
|
environment:
|
||||||
# XMPP Connection - uses internal Docker DNS names
|
# XMPP Connection
|
||||||
XMPP_SERVER: jeffsi-meet-prosody-1
|
XMPP_SERVER: ${XMPP_SERVER:-meet.jeffemmett.com}
|
||||||
XMPP_DOMAIN: meet.jitsi
|
XMPP_DOMAIN: ${XMPP_DOMAIN:-meet.jeffemmett.com}
|
||||||
XMPP_AUTH_DOMAIN: auth.meet.jitsi
|
XMPP_AUTH_DOMAIN: auth.${XMPP_DOMAIN:-meet.jeffemmett.com}
|
||||||
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
|
XMPP_INTERNAL_MUC_DOMAIN: internal.auth.${XMPP_DOMAIN:-meet.jeffemmett.com}
|
||||||
XMPP_RECORDER_DOMAIN: hidden.meet.jitsi
|
XMPP_RECORDER_DOMAIN: recorder.${XMPP_DOMAIN:-meet.jeffemmett.com}
|
||||||
XMPP_MUC_DOMAIN: muc.meet.jitsi
|
XMPP_MUC_DOMAIN: muc.${XMPP_DOMAIN:-meet.jeffemmett.com}
|
||||||
XMPP_TRUST_ALL_CERTS: "true"
|
|
||||||
|
|
||||||
# Jibri Settings
|
# Jibri Settings
|
||||||
JIBRI_BREWERY_MUC: JibriBrewery
|
JIBRI_BREWERY_MUC: JibriBrewery
|
||||||
|
|
@ -145,7 +145,7 @@ services:
|
||||||
CHROMIUM_FLAGS: --use-fake-ui-for-media-stream,--start-maximized,--kiosk,--enabled,--disable-infobars,--autoplay-policy=no-user-gesture-required
|
CHROMIUM_FLAGS: --use-fake-ui-for-media-stream,--start-maximized,--kiosk,--enabled,--disable-infobars,--autoplay-policy=no-user-gesture-required
|
||||||
|
|
||||||
# Public URL
|
# Public URL
|
||||||
PUBLIC_URL: https://meet.jeffemmett.com
|
PUBLIC_URL: https://${XMPP_DOMAIN:-meet.jeffemmett.com}
|
||||||
|
|
||||||
# Timezone
|
# Timezone
|
||||||
TZ: UTC
|
TZ: UTC
|
||||||
|
|
@ -161,7 +161,6 @@ services:
|
||||||
shm_size: 2gb
|
shm_size: 2gb
|
||||||
networks:
|
networks:
|
||||||
- meeting-intelligence
|
- meeting-intelligence
|
||||||
- jeffsi-meet
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|
@ -178,12 +177,10 @@ volumes:
|
||||||
type: none
|
type: none
|
||||||
o: bind
|
o: bind
|
||||||
device: /opt/meetings/audio
|
device: /opt/meetings/audio
|
||||||
|
whisper_models:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
meeting-intelligence:
|
meeting-intelligence:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
traefik-public:
|
traefik-public:
|
||||||
external: true
|
external: true
|
||||||
jeffsi-meet:
|
|
||||||
external: true
|
|
||||||
name: jeffsi-meet_meet.jitsi
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
cmake \
|
cmake \
|
||||||
git \
|
git \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
wget \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Build whisper.cpp
|
# Build whisper.cpp
|
||||||
|
|
@ -31,12 +30,10 @@ RUN cd /build/whisper.cpp && \
|
||||||
# Production image
|
# Production image
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Install runtime dependencies and build tools (for compiling Python packages)
|
# Install runtime dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
libsndfile1 \
|
libsndfile1 \
|
||||||
curl \
|
|
||||||
build-essential \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy whisper binary and models
|
# Copy whisper binary and models
|
||||||
|
|
@ -50,9 +47,6 @@ WORKDIR /app
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Remove build tools to reduce image size
|
|
||||||
RUN apt-get purge -y build-essential && apt-get autoremove -y
|
|
||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY app/ ./app/
|
COPY app/ ./app/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1385,8 +1385,6 @@
|
||||||
"sharedvideo": "Share video",
|
"sharedvideo": "Share video",
|
||||||
"sharedmusic": "Share music",
|
"sharedmusic": "Share music",
|
||||||
"stopSharedMusic": "Stop music",
|
"stopSharedMusic": "Stop music",
|
||||||
"meetingIntelligence": "Meeting Intelligence",
|
|
||||||
"closeMeetingIntelligence": "Close Meeting Intelligence",
|
|
||||||
"shortcuts": "Toggle shortcuts",
|
"shortcuts": "Toggle shortcuts",
|
||||||
"show": "Show on stage",
|
"show": "Show on stage",
|
||||||
"showWhiteboard": "Show whiteboard",
|
"showWhiteboard": "Show whiteboard",
|
||||||
|
|
@ -1503,8 +1501,6 @@
|
||||||
"sharedvideo": "Share video",
|
"sharedvideo": "Share video",
|
||||||
"sharedmusic": "Share music",
|
"sharedmusic": "Share music",
|
||||||
"stopSharedMusic": "Stop music",
|
"stopSharedMusic": "Stop music",
|
||||||
"meetingIntelligence": "Meeting Intelligence",
|
|
||||||
"closeMeetingIntelligence": "Close Meeting Intelligence",
|
|
||||||
"shortcuts": "View shortcuts",
|
"shortcuts": "View shortcuts",
|
||||||
"showWhiteboard": "Show whiteboard",
|
"showWhiteboard": "Show whiteboard",
|
||||||
"silence": "Silence",
|
"silence": "Silence",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,5 @@ import '../talk-while-muted/reducer';
|
||||||
import '../virtual-background/reducer';
|
import '../virtual-background/reducer';
|
||||||
import '../web-hid/reducer';
|
import '../web-hid/reducer';
|
||||||
import '../file-sharing/reducer';
|
import '../file-sharing/reducer';
|
||||||
import '../meeting-intelligence/reducer';
|
|
||||||
|
|
||||||
import './reducers.any';
|
import './reducers.any';
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ import { IJaaSState } from '../jaas/reducer';
|
||||||
import { IKeyboardShortcutsState } from '../keyboard-shortcuts/types';
|
import { IKeyboardShortcutsState } from '../keyboard-shortcuts/types';
|
||||||
import { ILargeVideoState } from '../large-video/reducer';
|
import { ILargeVideoState } from '../large-video/reducer';
|
||||||
import { ILobbyState } from '../lobby/reducer';
|
import { ILobbyState } from '../lobby/reducer';
|
||||||
import { IMeetingIntelligenceState } from '../meeting-intelligence/types';
|
|
||||||
import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
|
import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
|
||||||
import { IMobileBackgroundState } from '../mobile/background/reducer';
|
import { IMobileBackgroundState } from '../mobile/background/reducer';
|
||||||
import { ICallIntegrationState } from '../mobile/call-integration/reducer';
|
import { ICallIntegrationState } from '../mobile/call-integration/reducer';
|
||||||
|
|
@ -140,7 +139,6 @@ export interface IReduxState {
|
||||||
'features/keyboard-shortcuts': IKeyboardShortcutsState;
|
'features/keyboard-shortcuts': IKeyboardShortcutsState;
|
||||||
'features/large-video': ILargeVideoState;
|
'features/large-video': ILargeVideoState;
|
||||||
'features/lobby': ILobbyState;
|
'features/lobby': ILobbyState;
|
||||||
'features/meeting-intelligence': IMeetingIntelligenceState;
|
|
||||||
'features/mobile/audio-mode': IMobileAudioModeState;
|
'features/mobile/audio-mode': IMobileAudioModeState;
|
||||||
'features/mobile/background': IMobileBackgroundState;
|
'features/mobile/background': IMobileBackgroundState;
|
||||||
'features/mobile/external-api': IMobileExternalApiState;
|
'features/mobile/external-api': IMobileExternalApiState;
|
||||||
|
|
|
||||||
|
|
@ -478,13 +478,6 @@ export interface IConfig {
|
||||||
logging?: ILoggingConfig;
|
logging?: ILoggingConfig;
|
||||||
mainToolbarButtons?: Array<Array<string>>;
|
mainToolbarButtons?: Array<Array<string>>;
|
||||||
maxFullResolutionParticipants?: number;
|
maxFullResolutionParticipants?: number;
|
||||||
meetingIntelligence?: {
|
|
||||||
apiUrl?: string;
|
|
||||||
autoTranscribe?: boolean;
|
|
||||||
enabled?: boolean;
|
|
||||||
exportFormats?: Array<'pdf' | 'markdown' | 'json'>;
|
|
||||||
standalonePageEnabled?: boolean;
|
|
||||||
};
|
|
||||||
microsoftApiApplicationClientID?: string;
|
microsoftApiApplicationClientID?: string;
|
||||||
moderatedRoomServiceUrl?: string;
|
moderatedRoomServiceUrl?: string;
|
||||||
mouseMoveCallbackInterval?: number;
|
mouseMoveCallbackInterval?: number;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeIn
|
||||||
import LargeVideo from '../../../large-video/components/LargeVideo.web';
|
import LargeVideo from '../../../large-video/components/LargeVideo.web';
|
||||||
import LobbyScreen from '../../../lobby/components/web/LobbyScreen';
|
import LobbyScreen from '../../../lobby/components/web/LobbyScreen';
|
||||||
import { getIsLobbyVisible } from '../../../lobby/functions';
|
import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||||
import { MeetingIntelligenceDashboard } from '../../../meeting-intelligence/components';
|
|
||||||
import { getOverlayToRender } from '../../../overlay/functions.web';
|
import { getOverlayToRender } from '../../../overlay/functions.web';
|
||||||
import ParticipantsPane from '../../../participants-pane/components/web/ParticipantsPane';
|
import ParticipantsPane from '../../../participants-pane/components/web/ParticipantsPane';
|
||||||
import Prejoin from '../../../prejoin/components/web/Prejoin';
|
import Prejoin from '../../../prejoin/components/web/Prejoin';
|
||||||
|
|
@ -324,7 +323,6 @@ class Conference extends AbstractConference<IProps, any> {
|
||||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||||
</div>
|
</div>
|
||||||
<ParticipantsPane />
|
<ParticipantsPane />
|
||||||
<MeetingIntelligenceDashboard />
|
|
||||||
<ReactionAnimations />
|
<ReactionAnimations />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
/**
|
|
||||||
* Action types for Meeting Intelligence feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the meeting intelligence dashboard visibility.
|
|
||||||
*/
|
|
||||||
export const TOGGLE_MEETING_INTELLIGENCE = 'TOGGLE_MEETING_INTELLIGENCE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the active tab in the dashboard.
|
|
||||||
*/
|
|
||||||
export const SET_ACTIVE_TAB = 'SET_MEETING_INTELLIGENCE_ACTIVE_TAB';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a meeting for detailed view.
|
|
||||||
*/
|
|
||||||
export const SELECT_MEETING = 'SELECT_MEETING';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the selected meeting.
|
|
||||||
*/
|
|
||||||
export const CLEAR_SELECTED_MEETING = 'CLEAR_SELECTED_MEETING';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch meetings list.
|
|
||||||
*/
|
|
||||||
export const FETCH_MEETINGS_REQUEST = 'FETCH_MEETINGS_REQUEST';
|
|
||||||
export const FETCH_MEETINGS_SUCCESS = 'FETCH_MEETINGS_SUCCESS';
|
|
||||||
export const FETCH_MEETINGS_FAILURE = 'FETCH_MEETINGS_FAILURE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch transcript for a meeting.
|
|
||||||
*/
|
|
||||||
export const FETCH_TRANSCRIPT_REQUEST = 'FETCH_TRANSCRIPT_REQUEST';
|
|
||||||
export const FETCH_TRANSCRIPT_SUCCESS = 'FETCH_TRANSCRIPT_SUCCESS';
|
|
||||||
export const FETCH_TRANSCRIPT_FAILURE = 'FETCH_TRANSCRIPT_FAILURE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch summary for a meeting.
|
|
||||||
*/
|
|
||||||
export const FETCH_SUMMARY_REQUEST = 'FETCH_SUMMARY_REQUEST';
|
|
||||||
export const FETCH_SUMMARY_SUCCESS = 'FETCH_SUMMARY_SUCCESS';
|
|
||||||
export const FETCH_SUMMARY_FAILURE = 'FETCH_SUMMARY_FAILURE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate summary for a meeting.
|
|
||||||
*/
|
|
||||||
export const GENERATE_SUMMARY_REQUEST = 'GENERATE_SUMMARY_REQUEST';
|
|
||||||
export const GENERATE_SUMMARY_SUCCESS = 'GENERATE_SUMMARY_SUCCESS';
|
|
||||||
export const GENERATE_SUMMARY_FAILURE = 'GENERATE_SUMMARY_FAILURE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch speaker stats for a meeting.
|
|
||||||
*/
|
|
||||||
export const FETCH_SPEAKER_STATS_SUCCESS = 'FETCH_SPEAKER_STATS_SUCCESS';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search transcripts.
|
|
||||||
*/
|
|
||||||
export const SEARCH_REQUEST = 'MEETING_INTELLIGENCE_SEARCH_REQUEST';
|
|
||||||
export const SEARCH_SUCCESS = 'MEETING_INTELLIGENCE_SEARCH_SUCCESS';
|
|
||||||
export const SEARCH_FAILURE = 'MEETING_INTELLIGENCE_SEARCH_FAILURE';
|
|
||||||
export const SET_SEARCH_QUERY = 'SET_MEETING_INTELLIGENCE_SEARCH_QUERY';
|
|
||||||
export const CLEAR_SEARCH = 'CLEAR_MEETING_INTELLIGENCE_SEARCH';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export meeting.
|
|
||||||
*/
|
|
||||||
export const EXPORT_REQUEST = 'MEETING_INTELLIGENCE_EXPORT_REQUEST';
|
|
||||||
export const EXPORT_SUCCESS = 'MEETING_INTELLIGENCE_EXPORT_SUCCESS';
|
|
||||||
export const EXPORT_FAILURE = 'MEETING_INTELLIGENCE_EXPORT_FAILURE';
|
|
||||||
export const SET_EXPORT_FORMAT = 'SET_MEETING_INTELLIGENCE_EXPORT_FORMAT';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update meeting status (from polling).
|
|
||||||
*/
|
|
||||||
export const UPDATE_MEETING_STATUS = 'UPDATE_MEETING_STATUS';
|
|
||||||
|
|
@ -1,402 +0,0 @@
|
||||||
/**
|
|
||||||
* Action creators for Meeting Intelligence feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IStore } from '../app/types';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CLEAR_SEARCH,
|
|
||||||
CLEAR_SELECTED_MEETING,
|
|
||||||
EXPORT_FAILURE,
|
|
||||||
EXPORT_REQUEST,
|
|
||||||
EXPORT_SUCCESS,
|
|
||||||
FETCH_MEETINGS_FAILURE,
|
|
||||||
FETCH_MEETINGS_REQUEST,
|
|
||||||
FETCH_MEETINGS_SUCCESS,
|
|
||||||
FETCH_SPEAKER_STATS_SUCCESS,
|
|
||||||
FETCH_SUMMARY_FAILURE,
|
|
||||||
FETCH_SUMMARY_REQUEST,
|
|
||||||
FETCH_SUMMARY_SUCCESS,
|
|
||||||
FETCH_TRANSCRIPT_FAILURE,
|
|
||||||
FETCH_TRANSCRIPT_REQUEST,
|
|
||||||
FETCH_TRANSCRIPT_SUCCESS,
|
|
||||||
GENERATE_SUMMARY_FAILURE,
|
|
||||||
GENERATE_SUMMARY_REQUEST,
|
|
||||||
GENERATE_SUMMARY_SUCCESS,
|
|
||||||
SEARCH_FAILURE,
|
|
||||||
SEARCH_REQUEST,
|
|
||||||
SEARCH_SUCCESS,
|
|
||||||
SELECT_MEETING,
|
|
||||||
SET_ACTIVE_TAB,
|
|
||||||
SET_EXPORT_FORMAT,
|
|
||||||
SET_SEARCH_QUERY,
|
|
||||||
TOGGLE_MEETING_INTELLIGENCE,
|
|
||||||
UPDATE_MEETING_STATUS
|
|
||||||
} from './actionTypes';
|
|
||||||
import { API_BASE_URL } from './constants';
|
|
||||||
import {
|
|
||||||
IMeeting,
|
|
||||||
IMeetingSummary,
|
|
||||||
ISearchResult,
|
|
||||||
ISpeakerStats,
|
|
||||||
ITranscriptSegment
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the meeting intelligence dashboard.
|
|
||||||
*
|
|
||||||
* @returns {Object} The action object.
|
|
||||||
*/
|
|
||||||
export function toggleMeetingIntelligence() {
|
|
||||||
return {
|
|
||||||
type: TOGGLE_MEETING_INTELLIGENCE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the active tab.
|
|
||||||
*
|
|
||||||
* @param {string} tab - The tab to set active.
|
|
||||||
* @returns {Object} The action object.
|
|
||||||
*/
|
|
||||||
export function setActiveTab(tab: 'recordings' | 'transcript' | 'summary' | 'search') {
|
|
||||||
return {
|
|
||||||
type: SET_ACTIVE_TAB,
|
|
||||||
tab
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a meeting for detailed view.
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID to select.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function selectMeeting(meetingId: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: SELECT_MEETING, meetingId });
|
|
||||||
|
|
||||||
// Fetch transcript and summary for the selected meeting
|
|
||||||
dispatch(fetchTranscript(meetingId));
|
|
||||||
dispatch(fetchSummary(meetingId));
|
|
||||||
dispatch(fetchSpeakerStats(meetingId));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the selected meeting.
|
|
||||||
*
|
|
||||||
* @returns {Object} The action object.
|
|
||||||
*/
|
|
||||||
export function clearSelectedMeeting() {
|
|
||||||
return {
|
|
||||||
type: CLEAR_SELECTED_MEETING
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the list of meetings.
|
|
||||||
*
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function fetchMeetings() {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: FETCH_MEETINGS_REQUEST });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/meetings`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_MEETINGS_SUCCESS,
|
|
||||||
meetings: data.meetings as IMeeting[]
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_MEETINGS_FAILURE,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch meetings'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch transcript for a meeting.
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function fetchTranscript(meetingId: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: FETCH_TRANSCRIPT_REQUEST });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/transcript`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 404) {
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_TRANSCRIPT_SUCCESS,
|
|
||||||
transcript: []
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_TRANSCRIPT_SUCCESS,
|
|
||||||
transcript: data.segments as ITranscriptSegment[]
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_TRANSCRIPT_FAILURE,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch transcript'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch summary for a meeting.
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function fetchSummary(meetingId: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: FETCH_SUMMARY_REQUEST });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/summary`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 404) {
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_SUMMARY_SUCCESS,
|
|
||||||
summary: null
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_SUMMARY_SUCCESS,
|
|
||||||
summary: data as IMeetingSummary
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_SUMMARY_FAILURE,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch summary'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate AI summary for a meeting.
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function generateSummary(meetingId: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: GENERATE_SUMMARY_REQUEST });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/summary`, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: GENERATE_SUMMARY_SUCCESS,
|
|
||||||
summary: data as IMeetingSummary
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: GENERATE_SUMMARY_FAILURE,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to generate summary'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch speaker statistics for a meeting.
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function fetchSpeakerStats(meetingId: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/speakers`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: FETCH_SPEAKER_STATS_SUCCESS,
|
|
||||||
speakerStats: data.speakers as ISpeakerStats[]
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Silently ignore speaker stats errors
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search transcripts.
|
|
||||||
*
|
|
||||||
* @param {string} query - The search query.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function searchTranscripts(query: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: SET_SEARCH_QUERY, query });
|
|
||||||
|
|
||||||
if (!query.trim()) {
|
|
||||||
dispatch({ type: CLEAR_SEARCH });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: SEARCH_REQUEST });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/search`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ query, limit: 50 })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: SEARCH_SUCCESS,
|
|
||||||
results: data.results as ISearchResult[]
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: SEARCH_FAILURE,
|
|
||||||
error: error instanceof Error ? error.message : 'Search failed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear search results.
|
|
||||||
*
|
|
||||||
* @returns {Object} The action object.
|
|
||||||
*/
|
|
||||||
export function clearSearch() {
|
|
||||||
return { type: CLEAR_SEARCH };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set export format.
|
|
||||||
*
|
|
||||||
* @param {string} format - The export format.
|
|
||||||
* @returns {Object} The action object.
|
|
||||||
*/
|
|
||||||
export function setExportFormat(format: 'pdf' | 'markdown' | 'json') {
|
|
||||||
return {
|
|
||||||
type: SET_EXPORT_FORMAT,
|
|
||||||
format
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a meeting.
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID.
|
|
||||||
* @param {string} format - The export format.
|
|
||||||
* @returns {Function} Async thunk action.
|
|
||||||
*/
|
|
||||||
export function exportMeeting(meetingId: string, format: string) {
|
|
||||||
return async (dispatch: IStore['dispatch']) => {
|
|
||||||
dispatch({ type: EXPORT_REQUEST });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${API_BASE_URL}/meetings/${meetingId}/export?format=${format}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get filename from content-disposition header
|
|
||||||
const contentDisposition = response.headers.get('content-disposition');
|
|
||||||
let filename = `meeting-${meetingId}.${format === 'markdown' ? 'md' : format}`;
|
|
||||||
|
|
||||||
if (contentDisposition) {
|
|
||||||
const match = contentDisposition.match(/filename="(.+)"/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
filename = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download the file
|
|
||||||
const blob = await response.blob();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
dispatch({ type: EXPORT_SUCCESS });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: EXPORT_FAILURE,
|
|
||||||
error: error instanceof Error ? error.message : 'Export failed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update meeting status (from polling).
|
|
||||||
*
|
|
||||||
* @param {string} meetingId - The meeting ID.
|
|
||||||
* @param {string} status - The new status.
|
|
||||||
* @returns {Object} The action object.
|
|
||||||
*/
|
|
||||||
export function updateMeetingStatus(meetingId: string, status: string) {
|
|
||||||
return {
|
|
||||||
type: UPDATE_MEETING_STATUS,
|
|
||||||
meetingId,
|
|
||||||
status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from './index.web';
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export { default as MeetingIntelligenceButton } from './web/MeetingIntelligenceButton';
|
|
||||||
export { default as MeetingIntelligenceDashboard } from './web/MeetingIntelligenceDashboard';
|
|
||||||
export { default as RecordingsList } from './web/RecordingsList';
|
|
||||||
export { default as TranscriptViewer } from './web/TranscriptViewer';
|
|
||||||
export { default as SummaryPanel } from './web/SummaryPanel';
|
|
||||||
export { default as SearchPanel } from './web/SearchPanel';
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
|
||||||
import { translate } from '../../../base/i18n/functions';
|
|
||||||
import { IconMeter } from '../../../base/icons/svg';
|
|
||||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
|
||||||
import { toggleMeetingIntelligence } from '../../actions';
|
|
||||||
import { isDashboardOpen, isMeetingIntelligenceEnabled } from '../../functions';
|
|
||||||
|
|
||||||
interface IProps extends AbstractButtonProps {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the dashboard is open.
|
|
||||||
*/
|
|
||||||
_isDashboardOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements a button to open the Meeting Intelligence dashboard.
|
|
||||||
*/
|
|
||||||
class MeetingIntelligenceButton extends AbstractButton<IProps> {
|
|
||||||
override accessibilityLabel = 'toolbar.accessibilityLabel.meetingIntelligence';
|
|
||||||
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.closeMeetingIntelligence';
|
|
||||||
override icon = IconMeter;
|
|
||||||
override label = 'toolbar.meetingIntelligence';
|
|
||||||
override toggledLabel = 'toolbar.closeMeetingIntelligence';
|
|
||||||
override tooltip = 'toolbar.meetingIntelligence';
|
|
||||||
override toggledTooltip = 'toolbar.closeMeetingIntelligence';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles clicking / pressing the button.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
override _handleClick() {
|
|
||||||
this.props.dispatch(toggleMeetingIntelligence());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether this button is in toggled state or not.
|
|
||||||
*
|
|
||||||
* @override
|
|
||||||
* @protected
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
override _isToggled() {
|
|
||||||
return this.props._isDashboardOpen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps part of the Redux state to the props of this component.
|
|
||||||
*
|
|
||||||
* @param {Object} state - The Redux state.
|
|
||||||
* @private
|
|
||||||
* @returns {IProps}
|
|
||||||
*/
|
|
||||||
function _mapStateToProps(state: IReduxState) {
|
|
||||||
return {
|
|
||||||
_isDashboardOpen: isDashboardOpen(state),
|
|
||||||
visible: isMeetingIntelligenceEnabled(state)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(MeetingIntelligenceButton));
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
|
||||||
import {
|
|
||||||
clearSelectedMeeting,
|
|
||||||
fetchMeetings,
|
|
||||||
setActiveTab,
|
|
||||||
toggleMeetingIntelligence
|
|
||||||
} from '../../actions';
|
|
||||||
import { getMeetingIntelligenceState } from '../../functions';
|
|
||||||
|
|
||||||
import RecordingsList from './RecordingsList';
|
|
||||||
import SearchPanel from './SearchPanel';
|
|
||||||
import SummaryPanel from './SummaryPanel';
|
|
||||||
import TranscriptViewer from './TranscriptViewer';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Meeting Intelligence Dashboard side panel.
|
|
||||||
*
|
|
||||||
* @returns {React.ReactElement|null} The dashboard or null if closed.
|
|
||||||
*/
|
|
||||||
const MeetingIntelligenceDashboard: React.FC = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
activeTab,
|
|
||||||
selectedMeeting
|
|
||||||
} = useSelector((state: IReduxState) => getMeetingIntelligenceState(state));
|
|
||||||
|
|
||||||
// Fetch meetings on open
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
dispatch(fetchMeetings() as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch, isOpen ]);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
dispatch(toggleMeetingIntelligence());
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
|
||||||
dispatch(clearSelectedMeeting());
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
const handleTranscriptTab = useCallback(() => {
|
|
||||||
dispatch(setActiveTab('transcript'));
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
const handleSummaryTab = useCallback(() => {
|
|
||||||
dispatch(setActiveTab('summary'));
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
const handleRecordingsTab = useCallback(() => {
|
|
||||||
dispatch(setActiveTab('recordings'));
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
const handleSearchTab = useCallback(() => {
|
|
||||||
dispatch(setActiveTab('search'));
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'meeting-intelligence-dashboard'>
|
|
||||||
<div className = 'meeting-intelligence-header'>
|
|
||||||
<div className = 'meeting-intelligence-header-left'>
|
|
||||||
{selectedMeeting && (
|
|
||||||
<button
|
|
||||||
className = 'meeting-intelligence-back-btn'
|
|
||||||
onClick = { handleBack }
|
|
||||||
type = 'button'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '20'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '20'>
|
|
||||||
<path d = 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z' />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<h2>Meeting Intelligence</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className = 'meeting-intelligence-close-btn'
|
|
||||||
onClick = { handleClose }
|
|
||||||
type = 'button'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '24'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '24'>
|
|
||||||
<path d = 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z' />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedMeeting ? (
|
|
||||||
<>
|
|
||||||
<div className = 'meeting-intelligence-tabs'>
|
|
||||||
<button
|
|
||||||
className = { `tab-btn ${activeTab === 'transcript' ? 'active' : ''}` }
|
|
||||||
onClick = { handleTranscriptTab }
|
|
||||||
type = 'button'>
|
|
||||||
Transcript
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className = { `tab-btn ${activeTab === 'summary' ? 'active' : ''}` }
|
|
||||||
onClick = { handleSummaryTab }
|
|
||||||
type = 'button'>
|
|
||||||
Summary
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className = 'meeting-intelligence-content'>
|
|
||||||
{activeTab === 'transcript' && <TranscriptViewer />}
|
|
||||||
{activeTab === 'summary' && <SummaryPanel />}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className = 'meeting-intelligence-tabs'>
|
|
||||||
<button
|
|
||||||
className = { `tab-btn ${activeTab === 'recordings' ? 'active' : ''}` }
|
|
||||||
onClick = { handleRecordingsTab }
|
|
||||||
type = 'button'>
|
|
||||||
Recordings
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className = { `tab-btn ${activeTab === 'search' ? 'active' : ''}` }
|
|
||||||
onClick = { handleSearchTab }
|
|
||||||
type = 'button'>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className = 'meeting-intelligence-content'>
|
|
||||||
{activeTab === 'recordings' && <RecordingsList />}
|
|
||||||
{activeTab === 'search' && <SearchPanel />}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MeetingIntelligenceDashboard;
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
|
||||||
import { selectMeeting } from '../../actions';
|
|
||||||
import { formatDate, formatDuration, getMeetingIntelligenceState, getStatusColor, getStatusLabel } from '../../functions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of meeting recordings.
|
|
||||||
*
|
|
||||||
* @returns {React.ReactElement} The recordings list component.
|
|
||||||
*/
|
|
||||||
const RecordingsList: React.FC = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { meetings, meetingsLoading, meetingsError } = useSelector(
|
|
||||||
(state: IReduxState) => getMeetingIntelligenceState(state)
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectMeeting = useCallback((e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
const meetingId = e.currentTarget.dataset.meetingId;
|
|
||||||
|
|
||||||
if (meetingId) {
|
|
||||||
dispatch(selectMeeting(meetingId) as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
if (meetingsLoading) {
|
|
||||||
return (
|
|
||||||
<div className = 'recordings-loading'>
|
|
||||||
<div className = 'spinner' />
|
|
||||||
<span>Loading recordings...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meetingsError) {
|
|
||||||
return (
|
|
||||||
<div className = 'recordings-error'>
|
|
||||||
<p>Failed to load recordings: {meetingsError}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meetings.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className = 'recordings-empty'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '48'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '48'>
|
|
||||||
<path d = 'M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z' />
|
|
||||||
</svg>
|
|
||||||
<h3>No recordings yet</h3>
|
|
||||||
<p>Recordings will appear here after you record a meeting.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'recordings-list'>
|
|
||||||
{meetings.map(meeting => (
|
|
||||||
<div
|
|
||||||
className = 'recording-item'
|
|
||||||
data-meeting-id = { meeting.id }
|
|
||||||
key = { meeting.id }
|
|
||||||
onClick = { handleSelectMeeting }
|
|
||||||
onKeyDown = { handleSelectMeeting }
|
|
||||||
role = 'button'
|
|
||||||
tabIndex = { 0 }>
|
|
||||||
<div className = 'recording-item-icon'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '32'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '32'>
|
|
||||||
<path d = 'M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z' />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className = 'recording-item-info'>
|
|
||||||
<div className = 'recording-item-title'>
|
|
||||||
{meeting.title || meeting.conference_id}
|
|
||||||
</div>
|
|
||||||
<div className = 'recording-item-meta'>
|
|
||||||
{meeting.started_at && (
|
|
||||||
<span className = 'recording-date'>
|
|
||||||
{formatDate(meeting.started_at)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{meeting.duration_seconds && (
|
|
||||||
<span className = 'recording-duration'>
|
|
||||||
{formatDuration(meeting.duration_seconds)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className = 'recording-item-status'>
|
|
||||||
<span
|
|
||||||
className = 'status-badge'
|
|
||||||
style = {{ backgroundColor: getStatusColor(meeting.status) }}>
|
|
||||||
{getStatusLabel(meeting.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecordingsList;
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
|
||||||
import { searchTranscripts, selectMeeting } from '../../actions';
|
|
||||||
import { formatDate, formatTime, getMeetingIntelligenceState } from '../../functions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search panel for searching across all meeting transcripts.
|
|
||||||
*
|
|
||||||
* @returns {React.ReactElement} The search panel component.
|
|
||||||
*/
|
|
||||||
const SearchPanel: React.FC = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { searchQuery, searchResults, searchLoading, searchError } = useSelector(
|
|
||||||
(state: IReduxState) => getMeetingIntelligenceState(state)
|
|
||||||
);
|
|
||||||
const [ inputValue, setInputValue ] = useState(searchQuery);
|
|
||||||
|
|
||||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setInputValue(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dispatch(searchTranscripts(inputValue) as any);
|
|
||||||
}, [ dispatch, inputValue ]);
|
|
||||||
|
|
||||||
const handleResultClick = useCallback((e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
const meetingId = e.currentTarget.dataset.meetingId;
|
|
||||||
|
|
||||||
if (meetingId) {
|
|
||||||
dispatch(selectMeeting(meetingId) as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch ]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'search-panel'>
|
|
||||||
<form
|
|
||||||
className = 'search-form'
|
|
||||||
onSubmit = { handleSubmit }>
|
|
||||||
<input
|
|
||||||
className = 'search-input'
|
|
||||||
onChange = { handleInputChange }
|
|
||||||
placeholder = 'Search transcripts...'
|
|
||||||
type = 'text'
|
|
||||||
value = { inputValue } />
|
|
||||||
<button
|
|
||||||
className = 'search-button'
|
|
||||||
disabled = { searchLoading }
|
|
||||||
type = 'submit'>
|
|
||||||
{searchLoading ? (
|
|
||||||
<div className = 'spinner-small' />
|
|
||||||
) : (
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '20'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '20'>
|
|
||||||
<path d = 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z' />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{searchError && (
|
|
||||||
<div className = 'search-error'>
|
|
||||||
<p>Search failed: {searchError}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{searchResults.length > 0 && (
|
|
||||||
<div className = 'search-results'>
|
|
||||||
<div className = 'search-results-header'>
|
|
||||||
{searchResults.length} result{searchResults.length !== 1 ? 's' : ''} found
|
|
||||||
</div>
|
|
||||||
{searchResults.map((result, index) => (
|
|
||||||
<div
|
|
||||||
className = 'search-result-item'
|
|
||||||
data-meeting-id = { result.meeting_id }
|
|
||||||
key = { index }
|
|
||||||
onClick = { handleResultClick }
|
|
||||||
onKeyDown = { handleResultClick }
|
|
||||||
role = 'button'
|
|
||||||
tabIndex = { 0 }>
|
|
||||||
<div className = 'search-result-header'>
|
|
||||||
<span className = 'search-result-title'>
|
|
||||||
{result.title || result.conference_id}
|
|
||||||
</span>
|
|
||||||
{result.started_at && (
|
|
||||||
<span className = 'search-result-date'>
|
|
||||||
{formatDate(result.started_at)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className = 'search-result-snippet'>
|
|
||||||
{result.speaker_label && (
|
|
||||||
<span className = 'search-result-speaker'>
|
|
||||||
{result.speaker_label}:
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className = 'search-result-text'>
|
|
||||||
{result.segment_text}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className = 'search-result-meta'>
|
|
||||||
<span className = 'search-result-time'>
|
|
||||||
at {formatTime(result.start_time)}
|
|
||||||
</span>
|
|
||||||
<span className = 'search-result-score'>
|
|
||||||
Relevance: {Math.round(result.score * 100)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{searchQuery && searchResults.length === 0 && !searchLoading && !searchError && (
|
|
||||||
<div className = 'search-no-results'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '48'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '48'>
|
|
||||||
<path d = 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z' />
|
|
||||||
</svg>
|
|
||||||
<h3>No results found</h3>
|
|
||||||
<p>Try different keywords or check the spelling.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!searchQuery && (
|
|
||||||
<div className = 'search-hint'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '48'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '48'>
|
|
||||||
<path d = 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z' />
|
|
||||||
</svg>
|
|
||||||
<h3>Search across all meetings</h3>
|
|
||||||
<p>Find specific topics, decisions, or action items mentioned in any meeting.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchPanel;
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
|
||||||
import { exportMeeting, generateSummary } from '../../actions';
|
|
||||||
import { getMeetingIntelligenceState } from '../../functions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI-generated meeting summary panel.
|
|
||||||
*
|
|
||||||
* @returns {React.ReactElement} The summary panel component.
|
|
||||||
*/
|
|
||||||
const SummaryPanel: React.FC = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const {
|
|
||||||
summary,
|
|
||||||
summaryLoading,
|
|
||||||
summaryError,
|
|
||||||
selectedMeetingId,
|
|
||||||
selectedMeeting,
|
|
||||||
exportLoading
|
|
||||||
} = useSelector((state: IReduxState) => getMeetingIntelligenceState(state));
|
|
||||||
|
|
||||||
const handleGenerateSummary = useCallback(() => {
|
|
||||||
if (selectedMeetingId) {
|
|
||||||
dispatch(generateSummary(selectedMeetingId) as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch, selectedMeetingId ]);
|
|
||||||
|
|
||||||
const handleExportMarkdown = useCallback(() => {
|
|
||||||
if (selectedMeetingId) {
|
|
||||||
dispatch(exportMeeting(selectedMeetingId, 'markdown') as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch, selectedMeetingId ]);
|
|
||||||
|
|
||||||
const handleExportPdf = useCallback(() => {
|
|
||||||
if (selectedMeetingId) {
|
|
||||||
dispatch(exportMeeting(selectedMeetingId, 'pdf') as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch, selectedMeetingId ]);
|
|
||||||
|
|
||||||
const handleExportJson = useCallback(() => {
|
|
||||||
if (selectedMeetingId) {
|
|
||||||
dispatch(exportMeeting(selectedMeetingId, 'json') as any);
|
|
||||||
}
|
|
||||||
}, [ dispatch, selectedMeetingId ]);
|
|
||||||
|
|
||||||
if (summaryLoading) {
|
|
||||||
return (
|
|
||||||
<div className = 'summary-loading'>
|
|
||||||
<div className = 'spinner' />
|
|
||||||
<span>Loading summary...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (summaryError) {
|
|
||||||
return (
|
|
||||||
<div className = 'summary-error'>
|
|
||||||
<p>Failed to load summary: {summaryError}</p>
|
|
||||||
<button
|
|
||||||
className = 'btn-primary'
|
|
||||||
onClick = { handleGenerateSummary }
|
|
||||||
type = 'button'>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!summary) {
|
|
||||||
const canGenerate = selectedMeeting?.status === 'ready';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'summary-empty'>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '48'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '48'>
|
|
||||||
<path d = 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z' />
|
|
||||||
<path d = 'M18 9l-1.41-1.42L10 14.17l-2.59-2.58L6 13l4 4z' />
|
|
||||||
</svg>
|
|
||||||
<h3>No summary yet</h3>
|
|
||||||
<p>
|
|
||||||
{canGenerate
|
|
||||||
? 'Generate an AI summary to see key points, action items, and decisions.'
|
|
||||||
: 'Summary will be available after transcription is complete.'}
|
|
||||||
</p>
|
|
||||||
{canGenerate && (
|
|
||||||
<button
|
|
||||||
className = 'btn-primary'
|
|
||||||
onClick = { handleGenerateSummary }
|
|
||||||
type = 'button'>
|
|
||||||
Generate Summary
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'summary-panel'>
|
|
||||||
<div className = 'summary-section'>
|
|
||||||
<h3>Summary</h3>
|
|
||||||
<p className = 'summary-text'>{summary.summary_text}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{summary.key_points && summary.key_points.length > 0 && (
|
|
||||||
<div className = 'summary-section'>
|
|
||||||
<h3>Key Points</h3>
|
|
||||||
<ul className = 'key-points-list'>
|
|
||||||
{summary.key_points.map((point, index) => (
|
|
||||||
<li key = { index }>{point}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{summary.action_items && summary.action_items.length > 0 && (
|
|
||||||
<div className = 'summary-section'>
|
|
||||||
<h3>Action Items</h3>
|
|
||||||
<ul className = 'action-items-list'>
|
|
||||||
{summary.action_items.map((item, index) => (
|
|
||||||
<li key = { index }>
|
|
||||||
<span className = 'action-checkbox'>☐</span>
|
|
||||||
<span className = 'action-task'>{item.task}</span>
|
|
||||||
{item.assignee && (
|
|
||||||
<span className = 'action-assignee'>
|
|
||||||
({item.assignee})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{summary.decisions && summary.decisions.length > 0 && (
|
|
||||||
<div className = 'summary-section'>
|
|
||||||
<h3>Decisions</h3>
|
|
||||||
<ul className = 'decisions-list'>
|
|
||||||
{summary.decisions.map((decision, index) => (
|
|
||||||
<li key = { index }>{decision}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{summary.topics && summary.topics.length > 0 && (
|
|
||||||
<div className = 'summary-section'>
|
|
||||||
<h3>Topics Discussed</h3>
|
|
||||||
<div className = 'topics-tags'>
|
|
||||||
{summary.topics.map((topic, index) => (
|
|
||||||
<span
|
|
||||||
className = 'topic-tag'
|
|
||||||
key = { index }>
|
|
||||||
{topic}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className = 'summary-export'>
|
|
||||||
<h3>Export</h3>
|
|
||||||
<div className = 'export-buttons'>
|
|
||||||
<button
|
|
||||||
className = 'btn-secondary'
|
|
||||||
disabled = { exportLoading }
|
|
||||||
onClick = { handleExportMarkdown }
|
|
||||||
type = 'button'>
|
|
||||||
Markdown
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className = 'btn-secondary'
|
|
||||||
disabled = { exportLoading }
|
|
||||||
onClick = { handleExportPdf }
|
|
||||||
type = 'button'>
|
|
||||||
PDF
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className = 'btn-secondary'
|
|
||||||
disabled = { exportLoading }
|
|
||||||
onClick = { handleExportJson }
|
|
||||||
type = 'button'>
|
|
||||||
JSON
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SummaryPanel;
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
|
||||||
import {
|
|
||||||
formatTime,
|
|
||||||
getMeetingIntelligenceState,
|
|
||||||
getSpeakerColor,
|
|
||||||
getUniqueSpeakers,
|
|
||||||
groupSegmentsBySpeaker
|
|
||||||
} from '../../functions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transcript viewer component with speaker labels.
|
|
||||||
*
|
|
||||||
* @returns {React.ReactElement} The transcript viewer component.
|
|
||||||
*/
|
|
||||||
const TranscriptViewer: React.FC = () => {
|
|
||||||
const { transcript, transcriptLoading, transcriptError, selectedMeeting } = useSelector(
|
|
||||||
(state: IReduxState) => getMeetingIntelligenceState(state)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (transcriptLoading) {
|
|
||||||
return (
|
|
||||||
<div className = 'transcript-loading'>
|
|
||||||
<div className = 'spinner' />
|
|
||||||
<span>Loading transcript...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transcriptError) {
|
|
||||||
return (
|
|
||||||
<div className = 'transcript-error'>
|
|
||||||
<p>Failed to load transcript: {transcriptError}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transcript || transcript.length === 0) {
|
|
||||||
const isProcessing = selectedMeeting?.status !== 'ready' && selectedMeeting?.status !== 'failed';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'transcript-empty'>
|
|
||||||
{isProcessing ? (
|
|
||||||
<>
|
|
||||||
<div className = 'spinner' />
|
|
||||||
<h3>Transcript in progress</h3>
|
|
||||||
<p>The transcript is being generated. Please check back soon.</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg
|
|
||||||
fill = 'currentColor'
|
|
||||||
height = '48'
|
|
||||||
viewBox = '0 0 24 24'
|
|
||||||
width = '48'>
|
|
||||||
<path d = 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z' />
|
|
||||||
</svg>
|
|
||||||
<h3>No transcript available</h3>
|
|
||||||
<p>This meeting does not have a transcript yet.</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const speakerLabels = getUniqueSpeakers(transcript);
|
|
||||||
const groupedSegments = groupSegmentsBySpeaker(transcript);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'transcript-viewer'>
|
|
||||||
<div className = 'transcript-speakers'>
|
|
||||||
{speakerLabels.map(speaker => (
|
|
||||||
<span
|
|
||||||
className = 'speaker-tag'
|
|
||||||
key = { speaker }
|
|
||||||
style = {{ backgroundColor: getSpeakerColor(speaker, speakerLabels) }}>
|
|
||||||
{speaker}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className = 'transcript-segments'>
|
|
||||||
{groupedSegments.map((group, groupIndex) => (
|
|
||||||
<div
|
|
||||||
className = 'transcript-group'
|
|
||||||
key = { groupIndex }>
|
|
||||||
<div
|
|
||||||
className = 'transcript-speaker'
|
|
||||||
style = {{ color: getSpeakerColor(group.speaker, speakerLabels) }}>
|
|
||||||
{group.speaker}
|
|
||||||
<span className = 'transcript-time'>
|
|
||||||
{formatTime(group.segments[0].start_time)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className = 'transcript-text'>
|
|
||||||
{group.segments.map(segment => segment.text).join(' ')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TranscriptViewer;
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
/**
|
|
||||||
* Constants for Meeting Intelligence feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API base URL for meeting intelligence endpoints.
|
|
||||||
* Uses the integrated path on the same domain.
|
|
||||||
*/
|
|
||||||
export const API_BASE_URL = '/api/intelligence';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status to display text mapping.
|
|
||||||
*/
|
|
||||||
export const STATUS_LABELS: Record<string, string> = {
|
|
||||||
recording: 'Recording',
|
|
||||||
extracting_audio: 'Extracting Audio',
|
|
||||||
transcribing: 'Transcribing',
|
|
||||||
diarizing: 'Identifying Speakers',
|
|
||||||
summarizing: 'Generating Summary',
|
|
||||||
ready: 'Ready',
|
|
||||||
failed: 'Failed'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status to color mapping.
|
|
||||||
*/
|
|
||||||
export const STATUS_COLORS: Record<string, string> = {
|
|
||||||
recording: '#FF5722',
|
|
||||||
extracting_audio: '#2196F3',
|
|
||||||
transcribing: '#9C27B0',
|
|
||||||
diarizing: '#00BCD4',
|
|
||||||
summarizing: '#4CAF50',
|
|
||||||
ready: '#8BC34A',
|
|
||||||
failed: '#F44336'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speaker colors for transcript display.
|
|
||||||
*/
|
|
||||||
export const SPEAKER_COLORS = [
|
|
||||||
'#4FC3F7', // Light Blue
|
|
||||||
'#81C784', // Light Green
|
|
||||||
'#FFB74D', // Orange
|
|
||||||
'#F06292', // Pink
|
|
||||||
'#BA68C8', // Purple
|
|
||||||
'#4DB6AC', // Teal
|
|
||||||
'#FFD54F', // Amber
|
|
||||||
'#7986CB', // Indigo
|
|
||||||
'#A1887F', // Brown
|
|
||||||
'#90A4AE' // Blue Grey
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export format options.
|
|
||||||
*/
|
|
||||||
export const EXPORT_FORMATS = [
|
|
||||||
{ value: 'markdown', label: 'Markdown (.md)' },
|
|
||||||
{ value: 'pdf', label: 'PDF (.pdf)' },
|
|
||||||
{ value: 'json', label: 'JSON (.json)' }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Polling interval for meeting status updates (ms).
|
|
||||||
*/
|
|
||||||
export const STATUS_POLL_INTERVAL = 5000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum number of meetings to fetch per page.
|
|
||||||
*/
|
|
||||||
export const MEETINGS_PAGE_SIZE = 20;
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
/**
|
|
||||||
* Utility functions and selectors for Meeting Intelligence feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IReduxState } from '../app/types';
|
|
||||||
|
|
||||||
import { SPEAKER_COLORS, STATUS_COLORS, STATUS_LABELS } from './constants';
|
|
||||||
import { IMeetingIntelligenceState, ISpeakerStats, ITranscriptSegment } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the meeting intelligence state.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {IMeetingIntelligenceState} The meeting intelligence state.
|
|
||||||
*/
|
|
||||||
export function getMeetingIntelligenceState(state: IReduxState): IMeetingIntelligenceState {
|
|
||||||
return state['features/meeting-intelligence'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the dashboard is open.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {boolean} True if the dashboard is open.
|
|
||||||
*/
|
|
||||||
export function isDashboardOpen(state: IReduxState): boolean {
|
|
||||||
return getMeetingIntelligenceState(state)?.isOpen ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the active tab.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {string} The active tab name.
|
|
||||||
*/
|
|
||||||
export function getActiveTab(state: IReduxState): string {
|
|
||||||
return getMeetingIntelligenceState(state)?.activeTab ?? 'recordings';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the selected meeting.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {IMeeting|undefined} The selected meeting or undefined.
|
|
||||||
*/
|
|
||||||
export function getSelectedMeeting(state: IReduxState) {
|
|
||||||
return getMeetingIntelligenceState(state)?.selectedMeeting;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current transcript.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {ITranscriptSegment[]} The transcript segments.
|
|
||||||
*/
|
|
||||||
export function getTranscript(state: IReduxState): ITranscriptSegment[] {
|
|
||||||
return getMeetingIntelligenceState(state)?.transcript ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current summary.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {IMeetingSummary|undefined} The meeting summary or undefined.
|
|
||||||
*/
|
|
||||||
export function getSummary(state: IReduxState) {
|
|
||||||
return getMeetingIntelligenceState(state)?.summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get speaker statistics.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {ISpeakerStats[]} The speaker statistics array.
|
|
||||||
*/
|
|
||||||
export function getSpeakerStats(state: IReduxState): ISpeakerStats[] {
|
|
||||||
return getMeetingIntelligenceState(state)?.speakerStats ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format time in seconds to MM:SS or HH:MM:SS.
|
|
||||||
*
|
|
||||||
* @param {number} seconds - Time in seconds.
|
|
||||||
* @returns {string} Formatted time string.
|
|
||||||
*/
|
|
||||||
export function formatTime(seconds: number): string {
|
|
||||||
const totalSeconds = Math.floor(seconds);
|
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
|
||||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
||||||
const secs = totalSeconds % 60;
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format duration in seconds to human readable.
|
|
||||||
*
|
|
||||||
* @param {number} seconds - Duration in seconds.
|
|
||||||
* @returns {string} Human readable duration.
|
|
||||||
*/
|
|
||||||
export function formatDuration(seconds: number): string {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${minutes}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format date to display string.
|
|
||||||
*
|
|
||||||
* @param {string} dateString - ISO date string.
|
|
||||||
* @returns {string} Formatted date string.
|
|
||||||
*/
|
|
||||||
export function formatDate(dateString: string): string {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
|
|
||||||
return date.toLocaleDateString(undefined, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get color for a speaker based on label.
|
|
||||||
*
|
|
||||||
* @param {string} speakerLabel - The speaker label.
|
|
||||||
* @param {string[]} speakerLabels - All speaker labels.
|
|
||||||
* @returns {string} The color for the speaker.
|
|
||||||
*/
|
|
||||||
export function getSpeakerColor(speakerLabel: string, speakerLabels: string[]): string {
|
|
||||||
const index = speakerLabels.indexOf(speakerLabel);
|
|
||||||
|
|
||||||
return SPEAKER_COLORS[index % SPEAKER_COLORS.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get unique speaker labels from transcript.
|
|
||||||
*
|
|
||||||
* @param {ITranscriptSegment[]} transcript - The transcript segments.
|
|
||||||
* @returns {string[]} Unique speaker labels.
|
|
||||||
*/
|
|
||||||
export function getUniqueSpeakers(transcript: ITranscriptSegment[]): string[] {
|
|
||||||
const speakers = new Set<string>();
|
|
||||||
|
|
||||||
transcript.forEach(segment => {
|
|
||||||
if (segment.speaker_label) {
|
|
||||||
speakers.add(segment.speaker_label);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(speakers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get status label.
|
|
||||||
*
|
|
||||||
* @param {string} status - The status key.
|
|
||||||
* @returns {string} Human readable status label.
|
|
||||||
*/
|
|
||||||
export function getStatusLabel(status: string): string {
|
|
||||||
return STATUS_LABELS[status] ?? status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get status color.
|
|
||||||
*
|
|
||||||
* @param {string} status - The status key.
|
|
||||||
* @returns {string} The color hex code.
|
|
||||||
*/
|
|
||||||
export function getStatusColor(status: string): string {
|
|
||||||
return STATUS_COLORS[status] ?? '#9E9E9E';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group transcript segments by speaker.
|
|
||||||
*
|
|
||||||
* @param {ITranscriptSegment[]} transcript - The transcript segments.
|
|
||||||
* @returns {Array<{segments: ITranscriptSegment[], speaker: string}>} Grouped segments.
|
|
||||||
*/
|
|
||||||
export function groupSegmentsBySpeaker(
|
|
||||||
transcript: ITranscriptSegment[]
|
|
||||||
): Array<{ segments: ITranscriptSegment[]; speaker: string; }> {
|
|
||||||
const groups: Array<{ segments: ITranscriptSegment[]; speaker: string; }> = [];
|
|
||||||
let currentGroup: { segments: ITranscriptSegment[]; speaker: string; } | null = null;
|
|
||||||
|
|
||||||
transcript.forEach(segment => {
|
|
||||||
const speaker = segment.speaker_label || 'Speaker';
|
|
||||||
|
|
||||||
if (!currentGroup || currentGroup.speaker !== speaker) {
|
|
||||||
currentGroup = { speaker, segments: [] };
|
|
||||||
groups.push(currentGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGroup.segments.push(segment);
|
|
||||||
});
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if meeting intelligence is enabled in config.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {boolean} True if meeting intelligence is enabled.
|
|
||||||
*/
|
|
||||||
export function isMeetingIntelligenceEnabled(state: IReduxState): boolean {
|
|
||||||
const config = state['features/base/config'];
|
|
||||||
|
|
||||||
return config?.meetingIntelligence?.enabled ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the API URL from config or use default.
|
|
||||||
*
|
|
||||||
* @param {IReduxState} state - The Redux state.
|
|
||||||
* @returns {string} The API URL.
|
|
||||||
*/
|
|
||||||
export function getApiUrl(state: IReduxState): string {
|
|
||||||
const config = state['features/base/config'];
|
|
||||||
|
|
||||||
return config?.meetingIntelligence?.apiUrl ?? '/api/intelligence';
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import MeetingIntelligenceButton from './components/web/MeetingIntelligenceButton';
|
|
||||||
import { isMeetingIntelligenceEnabled } from './functions';
|
|
||||||
|
|
||||||
const meetingIntelligence = {
|
|
||||||
key: 'meetingintelligence',
|
|
||||||
Content: MeetingIntelligenceButton,
|
|
||||||
group: 3
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hook that returns the meeting intelligence button if it is enabled and undefined otherwise.
|
|
||||||
*
|
|
||||||
* @returns {Object | undefined}
|
|
||||||
*/
|
|
||||||
export function useMeetingIntelligenceButton() {
|
|
||||||
const meetingIntelligenceEnabled = useSelector(isMeetingIntelligenceEnabled);
|
|
||||||
|
|
||||||
if (meetingIntelligenceEnabled) {
|
|
||||||
return meetingIntelligence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { getLogger } from '../base/logging/functions';
|
|
||||||
|
|
||||||
export default getLogger('features/meeting-intelligence');
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
/**
|
|
||||||
* Reducer for Meeting Intelligence feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CLEAR_SEARCH,
|
|
||||||
CLEAR_SELECTED_MEETING,
|
|
||||||
EXPORT_FAILURE,
|
|
||||||
EXPORT_REQUEST,
|
|
||||||
EXPORT_SUCCESS,
|
|
||||||
FETCH_MEETINGS_FAILURE,
|
|
||||||
FETCH_MEETINGS_REQUEST,
|
|
||||||
FETCH_MEETINGS_SUCCESS,
|
|
||||||
FETCH_SPEAKER_STATS_SUCCESS,
|
|
||||||
FETCH_SUMMARY_FAILURE,
|
|
||||||
FETCH_SUMMARY_REQUEST,
|
|
||||||
FETCH_SUMMARY_SUCCESS,
|
|
||||||
FETCH_TRANSCRIPT_FAILURE,
|
|
||||||
FETCH_TRANSCRIPT_REQUEST,
|
|
||||||
FETCH_TRANSCRIPT_SUCCESS,
|
|
||||||
GENERATE_SUMMARY_FAILURE,
|
|
||||||
GENERATE_SUMMARY_REQUEST,
|
|
||||||
GENERATE_SUMMARY_SUCCESS,
|
|
||||||
SEARCH_FAILURE,
|
|
||||||
SEARCH_REQUEST,
|
|
||||||
SEARCH_SUCCESS,
|
|
||||||
SELECT_MEETING,
|
|
||||||
SET_ACTIVE_TAB,
|
|
||||||
SET_EXPORT_FORMAT,
|
|
||||||
SET_SEARCH_QUERY,
|
|
||||||
TOGGLE_MEETING_INTELLIGENCE,
|
|
||||||
UPDATE_MEETING_STATUS
|
|
||||||
} from './actionTypes';
|
|
||||||
import { IMeetingIntelligenceState } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initial state.
|
|
||||||
*/
|
|
||||||
const INITIAL_STATE: IMeetingIntelligenceState = {
|
|
||||||
isOpen: false,
|
|
||||||
activeTab: 'recordings',
|
|
||||||
meetings: [],
|
|
||||||
meetingsLoading: false,
|
|
||||||
meetingsError: undefined,
|
|
||||||
selectedMeetingId: undefined,
|
|
||||||
selectedMeeting: undefined,
|
|
||||||
transcript: [],
|
|
||||||
transcriptLoading: false,
|
|
||||||
transcriptError: undefined,
|
|
||||||
summary: undefined,
|
|
||||||
summaryLoading: false,
|
|
||||||
summaryError: undefined,
|
|
||||||
speakerStats: [],
|
|
||||||
searchQuery: '',
|
|
||||||
searchResults: [],
|
|
||||||
searchLoading: false,
|
|
||||||
searchError: undefined,
|
|
||||||
exportFormat: 'markdown',
|
|
||||||
exportLoading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reducer function.
|
|
||||||
*/
|
|
||||||
ReducerRegistry.register<IMeetingIntelligenceState>(
|
|
||||||
'features/meeting-intelligence',
|
|
||||||
(state = INITIAL_STATE, action): IMeetingIntelligenceState => {
|
|
||||||
switch (action.type) {
|
|
||||||
case TOGGLE_MEETING_INTELLIGENCE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isOpen: !state.isOpen
|
|
||||||
};
|
|
||||||
|
|
||||||
case SET_ACTIVE_TAB:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
activeTab: action.tab
|
|
||||||
};
|
|
||||||
|
|
||||||
case SELECT_MEETING:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedMeetingId: action.meetingId,
|
|
||||||
selectedMeeting: state.meetings.find(m => m.id === action.meetingId),
|
|
||||||
transcript: [],
|
|
||||||
summary: undefined,
|
|
||||||
speakerStats: [],
|
|
||||||
activeTab: 'transcript'
|
|
||||||
};
|
|
||||||
|
|
||||||
case CLEAR_SELECTED_MEETING:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedMeetingId: undefined,
|
|
||||||
selectedMeeting: undefined,
|
|
||||||
transcript: [],
|
|
||||||
summary: undefined,
|
|
||||||
speakerStats: [],
|
|
||||||
activeTab: 'recordings'
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_MEETINGS_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
meetingsLoading: true,
|
|
||||||
meetingsError: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_MEETINGS_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
meetings: action.meetings,
|
|
||||||
meetingsLoading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_MEETINGS_FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
meetingsLoading: false,
|
|
||||||
meetingsError: action.error
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_TRANSCRIPT_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
transcriptLoading: true,
|
|
||||||
transcriptError: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_TRANSCRIPT_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
transcript: action.transcript,
|
|
||||||
transcriptLoading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_TRANSCRIPT_FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
transcriptLoading: false,
|
|
||||||
transcriptError: action.error
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_SUMMARY_REQUEST:
|
|
||||||
case GENERATE_SUMMARY_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
summaryLoading: true,
|
|
||||||
summaryError: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_SUMMARY_SUCCESS:
|
|
||||||
case GENERATE_SUMMARY_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
summary: action.summary,
|
|
||||||
summaryLoading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_SUMMARY_FAILURE:
|
|
||||||
case GENERATE_SUMMARY_FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
summaryLoading: false,
|
|
||||||
summaryError: action.error
|
|
||||||
};
|
|
||||||
|
|
||||||
case FETCH_SPEAKER_STATS_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
speakerStats: action.speakerStats
|
|
||||||
};
|
|
||||||
|
|
||||||
case SET_SEARCH_QUERY:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
searchQuery: action.query
|
|
||||||
};
|
|
||||||
|
|
||||||
case SEARCH_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
searchLoading: true,
|
|
||||||
searchError: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
case SEARCH_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
searchResults: action.results,
|
|
||||||
searchLoading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
case SEARCH_FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
searchLoading: false,
|
|
||||||
searchError: action.error
|
|
||||||
};
|
|
||||||
|
|
||||||
case CLEAR_SEARCH:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
searchQuery: '',
|
|
||||||
searchResults: [],
|
|
||||||
searchError: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
case SET_EXPORT_FORMAT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
exportFormat: action.format
|
|
||||||
};
|
|
||||||
|
|
||||||
case EXPORT_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
exportLoading: true
|
|
||||||
};
|
|
||||||
|
|
||||||
case EXPORT_SUCCESS:
|
|
||||||
case EXPORT_FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
exportLoading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
case UPDATE_MEETING_STATUS: {
|
|
||||||
const updatedMeetings = state.meetings.map(m =>
|
|
||||||
m.id === action.meetingId
|
|
||||||
? { ...m, status: action.status }
|
|
||||||
: m
|
|
||||||
);
|
|
||||||
const updatedSelectedMeeting = state.selectedMeeting && state.selectedMeeting.id === action.meetingId
|
|
||||||
? { ...state.selectedMeeting, status: action.status }
|
|
||||||
: state.selectedMeeting;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
meetings: updatedMeetings,
|
|
||||||
selectedMeeting: updatedSelectedMeeting
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
/**
|
|
||||||
* Type definitions for Meeting Intelligence feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A single transcript segment with speaker and timing info.
|
|
||||||
*/
|
|
||||||
export interface ITranscriptSegment {
|
|
||||||
confidence?: number;
|
|
||||||
end_time: number;
|
|
||||||
id: string;
|
|
||||||
speaker_label?: string;
|
|
||||||
start_time: number;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An action item extracted from a meeting.
|
|
||||||
*/
|
|
||||||
export interface IActionItem {
|
|
||||||
assignee?: string;
|
|
||||||
due_date?: string;
|
|
||||||
task: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI-generated meeting summary.
|
|
||||||
*/
|
|
||||||
export interface IMeetingSummary {
|
|
||||||
action_items: IActionItem[];
|
|
||||||
decisions: string[];
|
|
||||||
generated_at: string;
|
|
||||||
key_points: string[];
|
|
||||||
sentiment?: string;
|
|
||||||
summary_text: string;
|
|
||||||
topics: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A meeting record.
|
|
||||||
*/
|
|
||||||
export interface IMeeting {
|
|
||||||
conference_id: string;
|
|
||||||
conference_name?: string;
|
|
||||||
created_at: string;
|
|
||||||
duration_seconds?: number;
|
|
||||||
ended_at?: string;
|
|
||||||
id: string;
|
|
||||||
recording_path?: string;
|
|
||||||
started_at?: string;
|
|
||||||
status: MeetingStatus;
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Meeting status values.
|
|
||||||
*/
|
|
||||||
export type MeetingStatus =
|
|
||||||
| 'recording'
|
|
||||||
| 'extracting_audio'
|
|
||||||
| 'transcribing'
|
|
||||||
| 'diarizing'
|
|
||||||
| 'summarizing'
|
|
||||||
| 'ready'
|
|
||||||
| 'failed';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speaker statistics.
|
|
||||||
*/
|
|
||||||
export interface ISpeakerStats {
|
|
||||||
percentage: number;
|
|
||||||
segment_count: number;
|
|
||||||
speaker_label: string;
|
|
||||||
total_duration: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search result item.
|
|
||||||
*/
|
|
||||||
export interface ISearchResult {
|
|
||||||
conference_id: string;
|
|
||||||
meeting_id: string;
|
|
||||||
score: number;
|
|
||||||
segment_text: string;
|
|
||||||
speaker_label?: string;
|
|
||||||
start_time: number;
|
|
||||||
started_at?: string;
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Meeting Intelligence Redux state.
|
|
||||||
*/
|
|
||||||
export interface IMeetingIntelligenceState {
|
|
||||||
activeTab: 'recordings' | 'transcript' | 'summary' | 'search';
|
|
||||||
// Export
|
|
||||||
exportFormat: 'pdf' | 'markdown' | 'json';
|
|
||||||
|
|
||||||
exportLoading: boolean;
|
|
||||||
// Dashboard state
|
|
||||||
isOpen: boolean;
|
|
||||||
// Meetings list
|
|
||||||
meetings: IMeeting[];
|
|
||||||
|
|
||||||
meetingsError?: string;
|
|
||||||
meetingsLoading: boolean;
|
|
||||||
|
|
||||||
searchError?: string;
|
|
||||||
searchLoading: boolean;
|
|
||||||
// Search
|
|
||||||
searchQuery: string;
|
|
||||||
|
|
||||||
searchResults: ISearchResult[];
|
|
||||||
selectedMeeting?: IMeeting;
|
|
||||||
// Selected meeting
|
|
||||||
selectedMeetingId?: string;
|
|
||||||
|
|
||||||
// Speaker stats
|
|
||||||
speakerStats: ISpeakerStats[];
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
summary?: IMeetingSummary;
|
|
||||||
summaryError?: string;
|
|
||||||
summaryLoading: boolean;
|
|
||||||
// Transcript
|
|
||||||
transcript: ITranscriptSegment[];
|
|
||||||
|
|
||||||
transcriptError?: string;
|
|
||||||
transcriptLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,6 @@ import { isGifEnabled } from '../gifs/function.any';
|
||||||
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
|
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
|
||||||
import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions';
|
import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions';
|
||||||
import { useKeyboardShortcutsButton } from '../keyboard-shortcuts/hooks';
|
import { useKeyboardShortcutsButton } from '../keyboard-shortcuts/hooks';
|
||||||
import { useMeetingIntelligenceButton } from '../meeting-intelligence/hooks';
|
|
||||||
import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
|
import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
|
||||||
import {
|
import {
|
||||||
close as closeParticipantsPane,
|
close as closeParticipantsPane,
|
||||||
|
|
@ -286,7 +285,6 @@ export function useToolboxButtons(
|
||||||
const shareaudio = getShareAudioButton();
|
const shareaudio = getShareAudioButton();
|
||||||
const shareVideo = useSharedVideoButton();
|
const shareVideo = useSharedVideoButton();
|
||||||
const shareMusic = useSharedMusicButton();
|
const shareMusic = useSharedMusicButton();
|
||||||
const meetingIntelligence = useMeetingIntelligenceButton();
|
|
||||||
const whiteboard = useWhiteboardButton();
|
const whiteboard = useWhiteboardButton();
|
||||||
const etherpad = useEtherpadButton();
|
const etherpad = useEtherpadButton();
|
||||||
const virtualBackground = useVirtualBackgroundButton();
|
const virtualBackground = useVirtualBackgroundButton();
|
||||||
|
|
@ -320,7 +318,6 @@ export function useToolboxButtons(
|
||||||
linktosalesforce,
|
linktosalesforce,
|
||||||
sharedvideo: shareVideo,
|
sharedvideo: shareVideo,
|
||||||
sharedmusic: shareMusic,
|
sharedmusic: shareMusic,
|
||||||
meetingintelligence: meetingIntelligence,
|
|
||||||
shareaudio,
|
shareaudio,
|
||||||
noisesuppression: noiseSuppression,
|
noisesuppression: noiseSuppression,
|
||||||
whiteboard,
|
whiteboard,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export type ToolbarButton = 'camera' |
|
||||||
'shareaudio' |
|
'shareaudio' |
|
||||||
'sharedvideo' |
|
'sharedvideo' |
|
||||||
'sharedmusic' |
|
'sharedmusic' |
|
||||||
'meetingintelligence' |
|
|
||||||
'shortcuts' |
|
'shortcuts' |
|
||||||
'stats' |
|
'stats' |
|
||||||
'tileview' |
|
'tileview' |
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@
|
||||||
"react/features/virtual-background",
|
"react/features/virtual-background",
|
||||||
"react/features/web-hid",
|
"react/features/web-hid",
|
||||||
"react/features/whiteboard",
|
"react/features/whiteboard",
|
||||||
"react/features/meeting-intelligence",
|
|
||||||
"**/web/*",
|
"**/web/*",
|
||||||
"**/*.web.ts",
|
"**/*.web.ts",
|
||||||
"**/*.web.tsx"
|
"**/*.web.tsx"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue