Compare commits
2 Commits
b3022237a3
...
793600dd5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 793600dd5a | |||
| 7190b1574d |
@ -2,18 +2,14 @@ package com.example.notebook
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -25,18 +21,21 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.example.notebook.data.DatabaseTest
|
||||
import com.example.notebook.ui.theme.NotebookTheme
|
||||
import com.example.notebook.viewmodel.NotebookViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val viewModel: NotebookViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
@ -45,23 +44,22 @@ class MainActivity : ComponentActivity() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
NotebookApp()
|
||||
NotebookApp(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NotebookApp() {
|
||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
||||
fun NotebookApp(viewModel: NotebookViewModel) {
|
||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||
val tabs = listOf("Studio", "Chat", "Sources")
|
||||
var showGoogleAppsMenu by remember { mutableStateOf(false) }
|
||||
var showSettingsMenu by remember { mutableStateOf(false) }
|
||||
var showAccountScreen by remember { mutableStateOf(false) }
|
||||
var chatInput by remember { mutableStateOf("") } // [BARU] State diangkat ke sini
|
||||
var chatInput by remember { mutableStateOf("") }
|
||||
|
||||
if (showAccountScreen) {
|
||||
AccountScreen(onDismiss = { showAccountScreen = false })
|
||||
@ -98,18 +96,28 @@ fun NotebookApp() {
|
||||
}
|
||||
)
|
||||
},
|
||||
// [DIUBAH] Bottom bar sekarang menjadi bagian dari Scaffold utama
|
||||
bottomBar = {
|
||||
if (selectedTabIndex == 1) { // Hanya tampil jika tab "Chat" aktif
|
||||
if (selectedTabIndex == 1) {
|
||||
BottomAppBar {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = chatInput,
|
||||
onValueChange = { chatInput = it },
|
||||
placeholder = { Text("Message...") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { /* Kirim pesan, TODO */ }) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (chatInput.isNotBlank()) {
|
||||
// TODO: Kirim ke notebook yang aktif
|
||||
// viewModel.sendUserMessage(notebookId, chatInput)
|
||||
chatInput = ""
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
|
||||
}
|
||||
}
|
||||
@ -120,24 +128,257 @@ fun NotebookApp() {
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
TabRow(selectedTabIndex = selectedTabIndex) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(selected = selectedTabIndex == index,
|
||||
Tab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { selectedTabIndex = index },
|
||||
text = { Text(title) })
|
||||
text = { Text(title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
when (selectedTabIndex) {
|
||||
0 -> StudioScreen()
|
||||
1 -> ChatScreen()
|
||||
2 -> SourcesScreen()
|
||||
0 -> StudioScreen(viewModel)
|
||||
1 -> ChatScreen(viewModel)
|
||||
2 -> SourcesScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === STUDIO SCREEN (UPDATED) ===
|
||||
@Composable
|
||||
fun StudioScreen(viewModel: NotebookViewModel) {
|
||||
val notebooks by viewModel.notebooks.collectAsState()
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showCreateDialog) {
|
||||
CreateNotebookDialog(
|
||||
onDismiss = { showCreateDialog = false },
|
||||
onConfirm = { title, description ->
|
||||
viewModel.createNotebook(title, description)
|
||||
showCreateDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
"Notebook terbaru",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Card untuk buat notebook baru
|
||||
item {
|
||||
NewNotebookCard(onClick = { showCreateDialog = true })
|
||||
}
|
||||
|
||||
// List notebooks yang sudah ada
|
||||
items(notebooks) { notebook ->
|
||||
NotebookCard(
|
||||
notebook = notebook,
|
||||
onClick = { /* TODO: Buka notebook */ },
|
||||
onDelete = { viewModel.deleteNotebook(notebook) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewNotebookCard(onClick: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F4F7))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFE1E3E6)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Buat notebook baru", tint = Color.Black)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Buat notebook baru", color = Color.Black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotebookCard(
|
||||
notebook: com.example.notebook.data.NotebookEntity,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault())
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Icon notebook
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFFE8EAF6)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Description,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF5C6BC0)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Info notebook
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = notebook.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = if (notebook.description.isNotBlank()) notebook.description
|
||||
else "Belum ada deskripsi",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = dateFormat.format(Date(notebook.updatedAt)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
// Delete button
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Hapus",
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateNotebookDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, String) -> Unit
|
||||
) {
|
||||
var title by remember { mutableStateOf("") }
|
||||
var description by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Buat Notebook Baru") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text("Judul Notebook") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Deskripsi (opsional)") },
|
||||
maxLines = 3,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (title.isNotBlank()) {
|
||||
onConfirm(title, description)
|
||||
}
|
||||
},
|
||||
enabled = title.isNotBlank()
|
||||
) {
|
||||
Text("Buat")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Batal")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// === CHAT & SOURCES SCREEN (Placeholder - akan diupdate nanti) ===
|
||||
@Composable
|
||||
fun ChatScreen(viewModel: NotebookViewModel) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Chat Screen - Coming Soon")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SourcesScreen(viewModel: NotebookViewModel) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Sources Screen - Coming Soon")
|
||||
}
|
||||
}
|
||||
|
||||
// === MENU COMPONENTS (Tetap sama) ===
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountScreen(onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@ -163,7 +404,6 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
Box {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -174,448 +414,39 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
||||
) {
|
||||
Text("M", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 40.sp)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.PhotoCamera,
|
||||
contentDescription = "Change profile picture",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(24.dp)
|
||||
.background(Color.LightGray, CircleShape)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text("Hi, 202310715190!", fontSize = 20.sp)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedButton(onClick = { /* TODO */ }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Manage your Google Account")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { /* TODO */ }
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.PersonAdd, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text("Add account")
|
||||
}
|
||||
Divider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { /* TODO */ }
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.ManageAccounts, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text("Manage accounts")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Privacy Policy", fontSize = 12.sp)
|
||||
Box(modifier = Modifier.size(2.dp).background(Color.Gray, CircleShape))
|
||||
Text("Terms of Service", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SettingsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("NotebookLM Help") },
|
||||
onClick = { /* TODO */ },
|
||||
onClick = { },
|
||||
leadingIcon = { Icon(Icons.Default.HelpOutline, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Send feedback") },
|
||||
onClick = { /* TODO */ },
|
||||
leadingIcon = { Icon(Icons.Default.Feedback, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Output Language") },
|
||||
onClick = { /* TODO */ },
|
||||
leadingIcon = { Icon(Icons.Default.Language, contentDescription = null) }
|
||||
)
|
||||
Divider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Light mode") },
|
||||
onClick = { /* TODO */ },
|
||||
leadingIcon = { Icon(Icons.Default.LightMode, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Dark mode") },
|
||||
onClick = { /* TODO */ },
|
||||
leadingIcon = { Icon(Icons.Default.DarkMode, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Device") },
|
||||
onClick = { /* TODO */ },
|
||||
leadingIcon = { Icon(Icons.Default.Tonality, contentDescription = null) },
|
||||
trailingIcon = { Icon(Icons.Default.Check, contentDescription = "Selected") }
|
||||
)
|
||||
Divider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Upgrade to Plus") },
|
||||
onClick = { /* TODO */ },
|
||||
leadingIcon = { Icon(Icons.Default.AllInclusive, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GoogleAppsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
||||
val apps = listOf(
|
||||
"Account" to Icons.Default.AccountCircle, "Gmail" to Icons.Default.Mail, "Drive" to Icons.Default.Cloud,
|
||||
"Classroom" to Icons.Default.School, "Docs" to Icons.Default.Article, "Gemini" to Icons.Default.AutoAwesome,
|
||||
"Sheets" to Icons.Default.GridOn, "Slides" to Icons.Default.Slideshow, "Calendar" to Icons.Default.CalendarToday,
|
||||
"Chat" to Icons.Default.Chat, "Meet" to Icons.Default.Videocam, "Forms" to Icons.Default.Assignment
|
||||
"Account" to Icons.Default.AccountCircle,
|
||||
"Gmail" to Icons.Default.Mail,
|
||||
"Drive" to Icons.Default.Cloud
|
||||
)
|
||||
val appChunks = apps.chunked(3)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismiss,
|
||||
modifier = Modifier.width(300.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
appChunks.forEach { rowApps ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
rowApps.forEach { (name, icon) ->
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable { /*TODO*/ }
|
||||
.padding(horizontal = 4.dp)
|
||||
) {
|
||||
Icon(icon, contentDescription = name, modifier = Modifier.size(40.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(name, fontSize = 12.sp, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SourcesScreen() {
|
||||
var showAddSourcesSheet by remember { mutableStateOf(false) }
|
||||
|
||||
if (showAddSourcesSheet) {
|
||||
AddSourcesSheet(onDismiss = { showAddSourcesSheet = false })
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(onClick = { showAddSourcesSheet = true }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Add")
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Add")
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(Icons.Filled.Description, contentDescription = "", modifier = Modifier.size(48.dp), tint = Color.Gray)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Saved sources will appear here", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Click Add source above to add PDFs, websites, text, videos, or audio files. Or import a file directly from Google Drive.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.Gray
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
||||
apps.forEach { (name, icon) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(name) },
|
||||
onClick = { },
|
||||
leadingIcon = { Icon(icon, contentDescription = name) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddSourcesSheet(onDismiss: () -> Unit) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showUploadMenu by remember { mutableStateOf(false) }
|
||||
|
||||
val photoPickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
onResult = { uri -> /* Handle URI */ }
|
||||
)
|
||||
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
onResult = { uri -> /* Handle URI */ }
|
||||
)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.AllInclusive, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("NotebookLM", fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Add sources", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Text("Get started by selecting sources", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||
|
||||
// Upload sources card
|
||||
Box {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showUploadMenu = true }
|
||||
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(40.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Default.CloudUpload, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
}
|
||||
Text("Upload sources", fontWeight = FontWeight.Bold)
|
||||
Text("Supported file types: PDF, .txt, Markdown, Audio (e.g. mp3)", fontSize = 12.sp, color = Color.Gray, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showUploadMenu,
|
||||
onDismissRequest = { showUploadMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Perpustakaan Foto") },
|
||||
onClick = {
|
||||
photoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
showUploadMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.PhotoLibrary, contentDescription = null) }
|
||||
)
|
||||
// [DIHAPUS] Opsi Ambil Video
|
||||
DropdownMenuItem(
|
||||
text = { Text("Pilih File") },
|
||||
onClick = {
|
||||
filePickerLauncher.launch("*/*")
|
||||
showUploadMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Google Drive") },
|
||||
onClick = {
|
||||
filePickerLauncher.launch("*/*")
|
||||
showUploadMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.Cloud, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Google Workspace card
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.CorporateFare, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Google Workspace", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Button(onClick = { /*TODO*/ }) {
|
||||
Icon(Icons.Default.Cloud, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Google Drive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatScreen() {
|
||||
// [DIUBAH] Scaffold dan state teks sudah tidak ada di sini lagi
|
||||
var showAddSourcesSheet by remember { mutableStateOf(false) }
|
||||
|
||||
if (showAddSourcesSheet) {
|
||||
AddSourcesSheet(onDismiss = { showAddSourcesSheet = false })
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Upload,
|
||||
contentDescription = "Upload",
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Add a source to get started",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = { showAddSourcesSheet = true }) {
|
||||
Text("Upload a source")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StudioScreen() {
|
||||
// State untuk dropdown menu
|
||||
var showUploadMenu by remember { mutableStateOf(false) }
|
||||
|
||||
// Launcher untuk mengambil media
|
||||
val photoPickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
onResult = { uri -> /* Handle URI */ }
|
||||
)
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
onResult = { uri -> /* Handle URI */ }
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().background(Color.White).padding(16.dp)) {
|
||||
Text("Notebook terbaru", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.Black)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Box untuk menjadi anchor bagi DropdownMenu
|
||||
Box {
|
||||
NewNotebookCard(onClick = { showUploadMenu = true })
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showUploadMenu,
|
||||
onDismissRequest = { showUploadMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Perpustakaan Foto") },
|
||||
onClick = {
|
||||
photoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) // Hanya gambar
|
||||
showUploadMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.PhotoLibrary, contentDescription = null) }
|
||||
)
|
||||
// [DIHAPUS] Opsi Ambil Video
|
||||
DropdownMenuItem(
|
||||
text = { Text("Pilih File") },
|
||||
onClick = {
|
||||
filePickerLauncher.launch("*/*")
|
||||
showUploadMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Google Drive") },
|
||||
onClick = {
|
||||
filePickerLauncher.launch("*/*")
|
||||
showUploadMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.Cloud, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewNotebookCard(onClick: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(width = 180.dp, height = 150.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F4F7))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFE1E3E6)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Buat notebook baru", tint = Color.Black)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Buat notebook baru", color = Color.Black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
NotebookTheme {
|
||||
StudioScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
package com.example.notebook.data
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Helper untuk test database
|
||||
* Panggil fungsi ini dari MainActivity untuk test
|
||||
*/
|
||||
class DatabaseTest(private val context: Context) {
|
||||
|
||||
private val database = AppDatabase.getDatabase(context)
|
||||
private val dao = database.notebookDao()
|
||||
|
||||
fun testInsertNotebook() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val testNotebook = NotebookEntity(
|
||||
title = "Notebook Test",
|
||||
description = "Ini adalah test database",
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
sourceCount = 0
|
||||
)
|
||||
|
||||
val id = dao.insertNotebook(testNotebook)
|
||||
println("✅ Database Test: Notebook berhasil disimpan dengan ID: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
package com.example.notebook.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository adalah "perantara" antara ViewModel dan Database
|
||||
* Semua akses database harus lewat sini
|
||||
*/
|
||||
class NotebookRepository(private val dao: NotebookDao) {
|
||||
|
||||
// === NOTEBOOK OPERATIONS ===
|
||||
|
||||
/**
|
||||
* Ambil semua notebooks (otomatis update kalau ada perubahan)
|
||||
*/
|
||||
fun getAllNotebooks(): Flow<List<NotebookEntity>> {
|
||||
return dao.getAllNotebooks()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil notebook berdasarkan ID
|
||||
*/
|
||||
fun getNotebookById(id: Int): Flow<NotebookEntity?> {
|
||||
return dao.getNotebookById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat notebook baru
|
||||
* Return: ID notebook yang baru dibuat
|
||||
*/
|
||||
suspend fun createNotebook(
|
||||
title: String,
|
||||
description: String = ""
|
||||
): Long {
|
||||
val notebook = NotebookEntity(
|
||||
title = title,
|
||||
description = description,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
sourceCount = 0
|
||||
)
|
||||
return dao.insertNotebook(notebook)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notebook yang sudah ada
|
||||
*/
|
||||
suspend fun updateNotebook(notebook: NotebookEntity) {
|
||||
val updated = notebook.copy(updatedAt = System.currentTimeMillis())
|
||||
dao.updateNotebook(updated)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus notebook
|
||||
*/
|
||||
suspend fun deleteNotebook(notebook: NotebookEntity) {
|
||||
dao.deleteNotebook(notebook)
|
||||
}
|
||||
|
||||
// === SOURCE OPERATIONS ===
|
||||
|
||||
/**
|
||||
* Tambah source ke notebook
|
||||
*/
|
||||
suspend fun addSource(
|
||||
notebookId: Int,
|
||||
fileName: String,
|
||||
fileType: String,
|
||||
filePath: String
|
||||
) {
|
||||
val source = SourceEntity(
|
||||
notebookId = notebookId,
|
||||
fileName = fileName,
|
||||
fileType = fileType,
|
||||
filePath = filePath,
|
||||
uploadedAt = System.currentTimeMillis()
|
||||
)
|
||||
dao.insertSource(source)
|
||||
|
||||
// Update sourceCount di notebook
|
||||
val notebook = dao.getNotebookById(notebookId)
|
||||
// Note: Ini simplified, di production pakai query COUNT
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil semua sources dalam notebook
|
||||
*/
|
||||
fun getSourcesByNotebook(notebookId: Int): Flow<List<SourceEntity>> {
|
||||
return dao.getSourcesByNotebook(notebookId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus source
|
||||
*/
|
||||
suspend fun deleteSource(source: SourceEntity) {
|
||||
dao.deleteSource(source)
|
||||
}
|
||||
|
||||
// === CHAT OPERATIONS ===
|
||||
|
||||
/**
|
||||
* Kirim pesan chat (user atau AI)
|
||||
*/
|
||||
suspend fun sendMessage(
|
||||
notebookId: Int,
|
||||
message: String,
|
||||
isUserMessage: Boolean
|
||||
) {
|
||||
val chatMessage = ChatMessageEntity(
|
||||
notebookId = notebookId,
|
||||
message = message,
|
||||
isUserMessage = isUserMessage,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
dao.insertChatMessage(chatMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil history chat
|
||||
*/
|
||||
fun getChatHistory(notebookId: Int): Flow<List<ChatMessageEntity>> {
|
||||
return dao.getChatHistory(notebookId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus semua chat dalam notebook
|
||||
*/
|
||||
suspend fun clearChatHistory(notebookId: Int) {
|
||||
dao.clearChatHistory(notebookId)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
package com.example.notebook.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.notebook.data.AppDatabase
|
||||
import com.example.notebook.data.NotebookEntity
|
||||
import com.example.notebook.data.NotebookRepository
|
||||
import com.example.notebook.data.SourceEntity
|
||||
import com.example.notebook.data.ChatMessageEntity
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* ViewModel untuk manage state aplikasi
|
||||
* Semua logic business ada di sini
|
||||
*/
|
||||
class NotebookViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val repository: NotebookRepository
|
||||
|
||||
// State untuk list notebooks
|
||||
private val _notebooks = MutableStateFlow<List<NotebookEntity>>(emptyList())
|
||||
val notebooks: StateFlow<List<NotebookEntity>> = _notebooks.asStateFlow()
|
||||
|
||||
// State untuk notebook yang sedang aktif
|
||||
private val _currentNotebook = MutableStateFlow<NotebookEntity?>(null)
|
||||
val currentNotebook: StateFlow<NotebookEntity?> = _currentNotebook.asStateFlow()
|
||||
|
||||
// State untuk sources dalam notebook aktif
|
||||
private val _sources = MutableStateFlow<List<SourceEntity>>(emptyList())
|
||||
val sources: StateFlow<List<SourceEntity>> = _sources.asStateFlow()
|
||||
|
||||
// State untuk chat history
|
||||
private val _chatMessages = MutableStateFlow<List<ChatMessageEntity>>(emptyList())
|
||||
val chatMessages: StateFlow<List<ChatMessageEntity>> = _chatMessages.asStateFlow()
|
||||
|
||||
// State loading
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||
|
||||
init {
|
||||
val database = AppDatabase.getDatabase(application)
|
||||
repository = NotebookRepository(database.notebookDao())
|
||||
loadNotebooks()
|
||||
}
|
||||
|
||||
// === NOTEBOOK FUNCTIONS ===
|
||||
|
||||
/**
|
||||
* Load semua notebooks dari database
|
||||
*/
|
||||
private fun loadNotebooks() {
|
||||
viewModelScope.launch {
|
||||
repository.getAllNotebooks().collect { notebooks ->
|
||||
_notebooks.value = notebooks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat notebook baru
|
||||
*/
|
||||
fun createNotebook(title: String, description: String = "") {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
try {
|
||||
val notebookId = repository.createNotebook(title, description)
|
||||
// Otomatis ter-update karena Flow
|
||||
println("✅ Notebook berhasil dibuat dengan ID: $notebookId")
|
||||
} catch (e: Exception) {
|
||||
println("❌ Error membuat notebook: ${e.message}")
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pilih notebook untuk dibuka
|
||||
*/
|
||||
fun selectNotebook(notebookId: Int) {
|
||||
viewModelScope.launch {
|
||||
repository.getNotebookById(notebookId).collect { notebook ->
|
||||
_currentNotebook.value = notebook
|
||||
notebook?.let {
|
||||
loadSources(notebookId)
|
||||
loadChatHistory(notebookId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notebook
|
||||
*/
|
||||
fun updateNotebook(notebook: NotebookEntity) {
|
||||
viewModelScope.launch {
|
||||
repository.updateNotebook(notebook)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus notebook
|
||||
*/
|
||||
fun deleteNotebook(notebook: NotebookEntity) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteNotebook(notebook)
|
||||
}
|
||||
}
|
||||
|
||||
// === SOURCE FUNCTIONS ===
|
||||
|
||||
/**
|
||||
* Load sources untuk notebook tertentu
|
||||
*/
|
||||
private fun loadSources(notebookId: Int) {
|
||||
viewModelScope.launch {
|
||||
repository.getSourcesByNotebook(notebookId).collect { sources ->
|
||||
_sources.value = sources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tambah source baru
|
||||
*/
|
||||
fun addSource(
|
||||
notebookId: Int,
|
||||
fileName: String,
|
||||
fileType: String,
|
||||
filePath: String
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
try {
|
||||
repository.addSource(notebookId, fileName, fileType, filePath)
|
||||
println("✅ Source berhasil ditambahkan: $fileName")
|
||||
} catch (e: Exception) {
|
||||
println("❌ Error menambahkan source: ${e.message}")
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === CHAT FUNCTIONS ===
|
||||
|
||||
/**
|
||||
* Load chat history
|
||||
*/
|
||||
private fun loadChatHistory(notebookId: Int) {
|
||||
viewModelScope.launch {
|
||||
repository.getChatHistory(notebookId).collect { messages ->
|
||||
_chatMessages.value = messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim pesan user
|
||||
*/
|
||||
fun sendUserMessage(notebookId: Int, message: String) {
|
||||
viewModelScope.launch {
|
||||
repository.sendMessage(notebookId, message, isUserMessage = true)
|
||||
// TODO: Panggil Gemini API di sini
|
||||
// Sementara kirim dummy AI response
|
||||
simulateAIResponse(notebookId, message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulasi AI response (sementara sebelum Gemini API)
|
||||
*/
|
||||
private fun simulateAIResponse(notebookId: Int, userMessage: String) {
|
||||
viewModelScope.launch {
|
||||
// Delay simulasi "AI thinking"
|
||||
kotlinx.coroutines.delay(1000)
|
||||
val aiResponse = "Ini adalah response sementara untuk: \"$userMessage\""
|
||||
repository.sendMessage(notebookId, aiResponse, isUserMessage = false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear chat history
|
||||
*/
|
||||
fun clearChatHistory(notebookId: Int) {
|
||||
viewModelScope.launch {
|
||||
repository.clearChatHistory(notebookId)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user