mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
add ability to update description in frontend
This commit is contained in:
parent
99cdf10e1c
commit
f220e4d3f3
@ -174,6 +174,8 @@ Events from the database. Accepts the following query string parameters:
|
|||||||
| `is_submitted` | int | Filter events that are submitted to Frigate+ (0 or 1) |
|
| `is_submitted` | int | Filter events that are submitted to Frigate+ (0 or 1) |
|
||||||
| `min_length` | float | Minimum length of the event |
|
| `min_length` | float | Minimum length of the event |
|
||||||
| `max_length` | float | Maximum length of the event |
|
| `max_length` | float | Maximum length of the event |
|
||||||
|
| `search` | str | Search query for semantic search |
|
||||||
|
| `like` | str | Event ID for thumbnail similarity search |
|
||||||
|
|
||||||
### `GET /api/timeline`
|
### `GET /api/timeline`
|
||||||
|
|
||||||
@ -225,7 +227,17 @@ Sub labels must be 100 characters or shorter.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"subLabel": "some_string",
|
"subLabel": "some_string",
|
||||||
"subLabelScore": 0.79,
|
"subLabelScore": 0.79
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `POST /api/events/<id>/description`
|
||||||
|
|
||||||
|
Set a description for an event to add details about what is occurring. Used during semantic search.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"description": "This is a black and white dog walking on the sidewalk."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -298,7 +310,7 @@ It is also possible to export this recording as a timelapse.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"playback": "realtime", // playback factor: realtime or timelapse_25x
|
"playback": "realtime" // playback factor: realtime or timelapse_25x
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ from frigate.const import (
|
|||||||
MAX_SEGMENT_DURATION,
|
MAX_SEGMENT_DURATION,
|
||||||
RECORD_DIR,
|
RECORD_DIR,
|
||||||
)
|
)
|
||||||
from frigate.embeddings import Embeddings
|
from frigate.embeddings import Embeddings, get_metadata
|
||||||
from frigate.events.external import ExternalEventProcessor
|
from frigate.events.external import ExternalEventProcessor
|
||||||
from frigate.models import Event, Previews, Recordings, Regions, Timeline
|
from frigate.models import Event, Previews, Recordings, Regions, Timeline
|
||||||
from frigate.object_processing import TrackedObject
|
from frigate.object_processing import TrackedObject
|
||||||
@ -449,6 +449,51 @@ def set_sub_label(id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/events/<id>/description", methods=("POST",))
|
||||||
|
def set_description(id):
|
||||||
|
try:
|
||||||
|
event: Event = Event.get(Event.id == id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||||
|
new_description = json.get("description")
|
||||||
|
|
||||||
|
if new_description is None or len(new_description) == 0:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "description cannot be empty",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.data["description"] = new_description
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
# If semantic search is enabled, update the index
|
||||||
|
if current_app.embeddings is not None:
|
||||||
|
current_app.embeddings.description.upsert(
|
||||||
|
documents=[new_description],
|
||||||
|
metadatas=[get_metadata(event)],
|
||||||
|
ids=[id],
|
||||||
|
)
|
||||||
|
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Event " + id + " description set to " + new_description,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/labels")
|
@bp.route("/labels")
|
||||||
def get_labels():
|
def get_labels():
|
||||||
camera = request.args.get("camera", type=str, default="")
|
camera = request.args.get("camera", type=str, default="")
|
||||||
|
|||||||
66
web-old/src/components/TextArea.jsx
Normal file
66
web-old/src/components/TextArea.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function TextArea({
|
||||||
|
helpText,
|
||||||
|
keyboardType = 'text',
|
||||||
|
inputRef,
|
||||||
|
onBlur,
|
||||||
|
onChangeText,
|
||||||
|
onFocus,
|
||||||
|
readonly,
|
||||||
|
value: propValue = '',
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const [isFocused, setFocused] = useState(false);
|
||||||
|
const [value, setValue] = useState(propValue);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setFocused(true);
|
||||||
|
onFocus && onFocus(event);
|
||||||
|
},
|
||||||
|
[onFocus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setFocused(false);
|
||||||
|
onBlur && onBlur(event);
|
||||||
|
},
|
||||||
|
[onBlur]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
setValue(value);
|
||||||
|
onChangeText && onChangeText(value);
|
||||||
|
},
|
||||||
|
[onChangeText, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (propValue !== value) {
|
||||||
|
setValue(propValue);
|
||||||
|
}
|
||||||
|
// DO NOT include `value`
|
||||||
|
}, [propValue, setValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<textarea
|
||||||
|
className="block p-2.5 w-full h-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
// className="h-6 mt-6 w-full bg-transparent border-0 focus:outline-none focus:ring-0"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onInput={handleChange}
|
||||||
|
readOnly={readonly}
|
||||||
|
tabIndex="0"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ import Dialog from '../components/Dialog';
|
|||||||
import MultiSelect from '../components/MultiSelect';
|
import MultiSelect from '../components/MultiSelect';
|
||||||
import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil';
|
import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil';
|
||||||
import TextField from '../components/TextField';
|
import TextField from '../components/TextField';
|
||||||
|
import TextArea from '../components/TextArea';
|
||||||
import TimeAgo from '../components/TimeAgo';
|
import TimeAgo from '../components/TimeAgo';
|
||||||
import Timepicker from '../components/TimePicker';
|
import Timepicker from '../components/TimePicker';
|
||||||
import TimelineSummary from '../components/TimelineSummary';
|
import TimelineSummary from '../components/TimelineSummary';
|
||||||
@ -259,6 +260,15 @@ export default function Events({ path, ...props }) {
|
|||||||
setState({ ...state, showDownloadMenu: true });
|
setState({ ...state, showDownloadMenu: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let descriptionTimeout;
|
||||||
|
const onUpdateDescription = async (event, description) => {
|
||||||
|
clearTimeout(descriptionTimeout);
|
||||||
|
descriptionTimeout = setTimeout(async () => {
|
||||||
|
event.data.description = description;
|
||||||
|
await axios.post(`events/${event.id}/description`, { description });
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const showSimilarEvents = (event_id, e) => {
|
const showSimilarEvents = (event_id, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -727,6 +737,7 @@ export default function Events({ path, ...props }) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
|
onUpdateDescription={onUpdateDescription}
|
||||||
showSimilarEvents={showSimilarEvents}
|
showSimilarEvents={showSimilarEvents}
|
||||||
showSubmitToPlus={showSubmitToPlus}
|
showSubmitToPlus={showSubmitToPlus}
|
||||||
/>
|
/>
|
||||||
@ -768,6 +779,7 @@ export default function Events({ path, ...props }) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
|
onUpdateDescription={onUpdateDescription}
|
||||||
showSimilarEvents={showSimilarEvents}
|
showSimilarEvents={showSimilarEvents}
|
||||||
showSubmitToPlus={showSubmitToPlus}
|
showSubmitToPlus={showSubmitToPlus}
|
||||||
/>
|
/>
|
||||||
@ -801,6 +813,7 @@ function Event({
|
|||||||
onDownloadClick,
|
onDownloadClick,
|
||||||
onReady,
|
onReady,
|
||||||
onSave,
|
onSave,
|
||||||
|
onUpdateDescription,
|
||||||
showSimilarEvents,
|
showSimilarEvents,
|
||||||
showSubmitToPlus,
|
showSubmitToPlus,
|
||||||
}) {
|
}) {
|
||||||
@ -837,7 +850,18 @@ function Event({
|
|||||||
{event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null}
|
{event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null}
|
||||||
</div>
|
</div>
|
||||||
{event?.data?.description ? (
|
{event?.data?.description ? (
|
||||||
<div className="text-sm flex flex-col grow pb-2">{event.data.description}</div>
|
<div className="flex flex-col pb-2">
|
||||||
|
{viewEvent === event.id ? (
|
||||||
|
<TextArea
|
||||||
|
label=""
|
||||||
|
value={event.data.description}
|
||||||
|
onChangeText={(value) => onUpdateDescription(event, value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="text-sm line-clamp-2">{event.data.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="text-sm flex">
|
<div className="text-sm flex">
|
||||||
<Clock className="h-5 w-5 mr-2 inline" />
|
<Clock className="h-5 w-5 mr-2 inline" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user